package schema

import (
	"encoding/json"
	"fmt"
	"strings"
)

func decodeRawKnowledgeGraph(raw map[string]json.RawMessage) (KnowledgeGraph, *ValidationResult) {
	graph := KnowledgeGraph{
		Version: rawString(raw["version"]),
		Kind:    rawString(raw["kind"]),
		Project: rawProjectMeta(raw["project"]),
	}

	if items, fatal := rawCollection(raw, "nodes", false); fatal != nil {
		return KnowledgeGraph{}, fatal
	} else if items != nil {
		graph.Nodes = make([]GraphNode, 0, len(items))
		for _, item := range items {
			graph.Nodes = append(graph.Nodes, rawGraphNode(item))
		}
	}
	if items, fatal := rawCollection(raw, "edges", false); fatal != nil {
		return KnowledgeGraph{}, fatal
	} else if items != nil {
		graph.Edges = make([]GraphEdge, 0, len(items))
		for _, item := range items {
			graph.Edges = append(graph.Edges, rawGraphEdge(item))
		}
	}
	if items, fatal := rawCollection(raw, "layers", true); fatal != nil {
		return KnowledgeGraph{}, fatal
	} else if items != nil {
		graph.Layers = make([]Layer, 0, len(items))
		for _, item := range items {
			graph.Layers = append(graph.Layers, rawLayer(item))
		}
	}
	if items, fatal := rawCollection(raw, "tour", true); fatal != nil {
		return KnowledgeGraph{}, fatal
	} else if items != nil {
		graph.Tour = make([]TourStep, 0, len(items))
		for _, item := range items {
			graph.Tour = append(graph.Tour, rawTourStep(item))
		}
	}
	return graph, nil
}

func rawCollection(raw map[string]json.RawMessage, name string, nullAsEmpty bool) ([]json.RawMessage, *ValidationResult) {
	value, ok := raw[name]
	if !ok {
		return nil, nil
	}
	if isJSONNull(value) {
		if nullAsEmpty {
			return []json.RawMessage{}, nil
		}
		return nil, invalidCollectionResult(name)
	}
	var items []json.RawMessage
	if err := json.Unmarshal(value, &items); err != nil {
		return nil, invalidCollectionResult(name)
	}
	return items, nil
}

func invalidCollectionResult(name string) *ValidationResult {
	issue := buildInvalidCollectionIssue(name)
	result := ValidationResult{Success: false, Issues: []GraphIssue{issue}, Fatal: issue.Message}
	return &result
}

func buildInvalidCollectionIssue(name string) GraphIssue {
	return GraphIssue{
		Level:    IssueFatal,
		Category: "invalid-collection",
		Message:  fmt.Sprintf("%q must be an array when present", name),
		Path:     name,
	}
}

func rawProjectMeta(data json.RawMessage) ProjectMeta {
	fields := rawObject(data)
	if fields == nil {
		return ProjectMeta{}
	}
	languages, languagesOK := rawStringArray(fields["languages"])
	frameworks, frameworksOK := rawStringArray(fields["frameworks"])
	project := ProjectMeta{
		Name:          rawString(fields["name"]),
		Description:   rawString(fields["description"]),
		AnalyzedAt:    rawString(fields["analyzedAt"]),
		GitCommitHash: rawString(fields["gitCommitHash"]),
	}
	if languagesOK {
		project.Languages = languages
	}
	if frameworksOK {
		project.Frameworks = frameworks
	}
	return project
}

func rawGraphNode(data json.RawMessage) GraphNode {
	fields := rawObject(data)
	if fields == nil {
		return GraphNode{Type: "file", Complexity: "moderate", Tags: []string{}, Summary: "invalid node", shapeInvalid: "node must be an object"}
	}
	shapeInvalid := ""
	domainMeta, hasDomainMeta := rawDomainMeta(fields["domainMeta"])
	knowledgeMeta, hasKnowledgeMeta := rawKnowledgeMeta(fields["knowledgeMeta"])
	if len(fields["lineRange"]) != 0 {
		if _, invalid := rawLineRange(fields["lineRange"]); invalid {
			shapeInvalid = "lineRange must contain exactly two numbers"
		}
	}
	if hasDomainMeta && domainMeta.shapeInvalid != "" && shapeInvalid == "" {
		shapeInvalid = domainMeta.shapeInvalid
	}
	if hasKnowledgeMeta && knowledgeMeta.shapeInvalid != "" && shapeInvalid == "" {
		shapeInvalid = knowledgeMeta.shapeInvalid
	}
	node := GraphNode{
		ID:            rawString(fields["id"]),
		Type:          rawString(fields["type"]),
		Name:          rawString(fields["name"]),
		FilePath:      rawString(fields["filePath"]),
		Summary:       rawString(fields["summary"]),
		Complexity:    rawString(fields["complexity"]),
		LanguageNotes: rawString(fields["languageNotes"]),
		Extra:         rawExtra(fields, "id", "type", "name", "filePath", "lineRange", "summary", "tags", "complexity", "languageNotes", "domainMeta", "knowledgeMeta"),
		shapeInvalid:  shapeInvalid,
	}
	if tags, ok := rawStringArray(fields["tags"]); ok {
		node.Tags = tags
	} else if rawArrayPresent(fields["tags"]) && shapeInvalid == "" {
		node.shapeInvalid = "tags must be an array of strings"
	}
	if lineRange, invalid := rawLineRange(fields["lineRange"]); invalid {
		node.LineRange = []int{0}
	} else {
		node.LineRange = lineRange
	}
	if hasDomainMeta {
		node.DomainMeta = domainMeta
	}
	if hasKnowledgeMeta {
		node.KnowledgeMeta = knowledgeMeta
	}
	return node
}

func rawDomainMeta(data json.RawMessage) (*DomainMeta, bool) {
	if len(data) == 0 || isJSONNull(data) {
		return nil, false
	}
	fields := rawObject(data)
	if fields == nil {
		return &DomainMeta{shapeInvalid: "domainMeta must be an object"}, true
	}
	meta := &DomainMeta{
		EntryPoint: rawString(fields["entryPoint"]),
		EntryType:  rawString(fields["entryType"]),
		Extra:      rawExtra(fields, "entities", "businessRules", "crossDomainInteractions", "entryPoint", "entryType"),
	}
	if values, ok := rawStringArray(fields["entities"]); ok {
		meta.Entities = values
	} else if rawFieldPresent(fields["entities"]) {
		meta.shapeInvalid = "domainMeta.entities must be an array of strings"
	}
	if values, ok := rawStringArray(fields["businessRules"]); ok {
		meta.BusinessRules = values
	} else if rawFieldPresent(fields["businessRules"]) && meta.shapeInvalid == "" {
		meta.shapeInvalid = "domainMeta.businessRules must be an array of strings"
	}
	if values, ok := rawStringArray(fields["crossDomainInteractions"]); ok {
		meta.CrossDomainInteractions = values
	} else if rawFieldPresent(fields["crossDomainInteractions"]) && meta.shapeInvalid == "" {
		meta.shapeInvalid = "domainMeta.crossDomainInteractions must be an array of strings"
	}
	return meta, true
}

func rawKnowledgeMeta(data json.RawMessage) (*KnowledgeMeta, bool) {
	if len(data) == 0 || isJSONNull(data) {
		return nil, false
	}
	fields := rawObject(data)
	if fields == nil {
		return &KnowledgeMeta{shapeInvalid: "knowledgeMeta must be an object"}, true
	}
	meta := &KnowledgeMeta{
		Category: rawString(fields["category"]),
		Content:  rawString(fields["content"]),
		Extra:    rawExtra(fields, "wikilinks", "backlinks", "category", "content"),
	}
	if values, ok := rawStringArray(fields["wikilinks"]); ok {
		meta.Wikilinks = values
	} else if rawFieldPresent(fields["wikilinks"]) {
		meta.shapeInvalid = "knowledgeMeta.wikilinks must be an array of strings"
	}
	if values, ok := rawStringArray(fields["backlinks"]); ok {
		meta.Backlinks = values
	} else if rawFieldPresent(fields["backlinks"]) && meta.shapeInvalid == "" {
		meta.shapeInvalid = "knowledgeMeta.backlinks must be an array of strings"
	}
	return meta, true
}

func rawGraphEdge(data json.RawMessage) GraphEdge {
	fields := rawObject(data)
	if fields == nil {
		return GraphEdge{Direction: "forward", Weight: 0.5, shapeInvalid: "edge must be an object"}
	}
	weight, weightInvalid, _ := parseWeight(fields["weight"])
	return GraphEdge{
		Source:        rawString(fields["source"]),
		Target:        rawString(fields["target"]),
		Type:          rawString(fields["type"]),
		Direction:     rawString(fields["direction"]),
		Description:   rawString(fields["description"]),
		Weight:        weight,
		weightInvalid: weightInvalid,
	}
}

func rawLayer(data json.RawMessage) Layer {
	fields := rawObject(data)
	if fields == nil {
		return Layer{shapeInvalid: "layer must be an object"}
	}
	nodeIDs, ok := rawStringArray(fields["nodeIds"])
	if !ok {
		nodeIDs = nil
	}
	return Layer{ID: rawString(fields["id"]), Name: rawString(fields["name"]), Description: rawString(fields["description"]), NodeIDs: nodeIDs}
}

func rawTourStep(data json.RawMessage) TourStep {
	fields := rawObject(data)
	if fields == nil {
		return TourStep{shapeInvalid: "tour step must be an object"}
	}
	nodeIDs, ok := rawStringArray(fields["nodeIds"])
	if !ok {
		nodeIDs = nil
	}
	return TourStep{Order: rawInt(fields["order"]), Title: rawString(fields["title"]), Description: rawString(fields["description"]), NodeIDs: nodeIDs, LanguageLesson: rawString(fields["languageLesson"])}
}

func rawObject(data json.RawMessage) map[string]json.RawMessage {
	if len(data) == 0 || isJSONNull(data) {
		return nil
	}
	var fields map[string]json.RawMessage
	if err := json.Unmarshal(data, &fields); err != nil {
		return nil
	}
	return fields
}

func rawString(data json.RawMessage) string {
	var value string
	if len(data) == 0 || isJSONNull(data) || json.Unmarshal(data, &value) != nil {
		return ""
	}
	return value
}

func rawInt(data json.RawMessage) int {
	var value int
	if len(data) == 0 || isJSONNull(data) || json.Unmarshal(data, &value) != nil {
		return 0
	}
	return value
}

func rawStringArray(data json.RawMessage) ([]string, bool) {
	if len(data) == 0 || isJSONNull(data) {
		return nil, false
	}
	var rawItems []json.RawMessage
	if err := json.Unmarshal(data, &rawItems); err != nil {
		return nil, false
	}
	items := make([]string, 0, len(rawItems))
	for _, rawItem := range rawItems {
		var value string
		if err := json.Unmarshal(rawItem, &value); err != nil {
			return nil, false
		}
		items = append(items, value)
	}
	return items, true
}

func rawArrayPresent(data json.RawMessage) bool {
	if len(data) == 0 || isJSONNull(data) {
		return false
	}
	var items []json.RawMessage
	return json.Unmarshal(data, &items) == nil
}

func rawFieldPresent(data json.RawMessage) bool {
	return len(data) != 0 && !isJSONNull(data)
}

func rawLineRange(data json.RawMessage) ([]int, bool) {
	if len(data) == 0 || isJSONNull(data) {
		return nil, false
	}
	var values []float64
	if err := json.Unmarshal(data, &values); err != nil {
		return nil, true
	}
	if len(values) != 2 {
		return nil, true
	}
	return []int{int(values[0]), int(values[1])}, false
}

func rawExtra(fields map[string]json.RawMessage, known ...string) map[string]JSON {
	extra := make(map[string]json.RawMessage, len(fields))
	for key, value := range fields {
		extra[key] = append([]byte(nil), value...)
	}
	deleteKnown(extra, known...)
	return nonEmptyExtra(extra)
}

func isJSONNull(data json.RawMessage) bool {
	return strings.TrimSpace(string(data)) == "null"
}

func invalidTopLevelCollectionIssue(graph KnowledgeGraph) (GraphIssue, bool) {
	for _, collection := range []struct {
		name  string
		valid bool
	}{
		{name: "nodes", valid: graph.Nodes != nil},
		{name: "edges", valid: graph.Edges != nil},
		{name: "layers", valid: graph.Layers != nil},
		{name: "tour", valid: graph.Tour != nil},
	} {
		if !collection.valid {
			return buildInvalidCollectionIssue(collection.name), true
		}
	}
	return GraphIssue{}, false
}
