package plan

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"regexp"
	"sort"
	"strconv"
	"strings"

	"gopkg.in/yaml.v3"
)

var yamlLinePattern = regexp.MustCompile(`line ([0-9]+):`)

type position struct {
	path   string
	line   int
	column int
}

type sourceIndex struct {
	byPath map[string]position
	byLine map[int]position
	nodes  map[int]string
	edges  map[int]string
	merges map[string]struct{}
	root   *yaml.Node
}

// Parse strictly decodes one PlanVersion YAML document. YAML/schema failures
// are diagnostics rather than Go errors because they are author feedback.
func Parse(raw []byte) (*Document, []Diagnostic) {
	if diagnostics := sourceBudgetDiagnostics(raw); len(diagnostics) != 0 {
		return nil, diagnostics
	}
	index := indexSource(raw)
	decoder := yaml.NewDecoder(bytes.NewReader(raw))
	decoder.KnownFields(true)

	var document Document
	if err := decoder.Decode(&document); err != nil {
		if errors.Is(err, io.EOF) {
			return nil, []Diagnostic{{Severity: SeverityError, Code: "syntax.empty", Path: "$", Message: "PlanVersion document is empty"}}
		}
		diagnostics := yamlDiagnostics(err, index)
		sortDiagnostics(diagnostics)
		return &document, diagnostics
	}

	var extra yaml.Node
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		diagnostic := Diagnostic{Severity: SeverityError, Code: "syntax.multiple_documents", Path: "$", Message: "exactly one YAML document is allowed"}
		if err != nil {
			diagnostic.Message = err.Error()
		}
		if extra.Line > 0 {
			diagnostic.Line, diagnostic.Column = extra.Line, extra.Column
		}
		return &document, []Diagnostic{diagnostic}
	}
	var diagnostics []Diagnostic
	if document.APIVersion != APIVersion {
		diagnostics = append(diagnostics, index.decorate(Diagnostic{
			Severity: SeverityError, Code: "header.api_version", Path: "apiVersion",
			Message: fmt.Sprintf("apiVersion must be %q", APIVersion),
		}))
	}
	if document.Kind != Kind {
		diagnostics = append(diagnostics, index.decorate(Diagnostic{
			Severity: SeverityError, Code: "header.kind", Path: "kind",
			Message: fmt.Sprintf("kind must be %q", Kind),
		}))
	}
	mergePaths := make([]string, 0, len(index.merges))
	for path := range index.merges {
		mergePaths = append(mergePaths, path)
	}
	sort.Strings(mergePaths)
	for _, path := range mergePaths {
		diagnostics = append(diagnostics, index.decorate(Diagnostic{
			Severity: SeverityError,
			Code:     "syntax.unsupported_merge_key",
			Path:     path,
			Message:  "YAML merge keys are not supported; author the fields directly",
		}))
	}
	diagnostics = append(diagnostics, sourcePresenceDiagnostics(&document, index)...)
	sortDiagnostics(diagnostics)
	return &document, diagnostics
}

func sourcePresenceDiagnostics(document *Document, index *sourceIndex) []Diagnostic {
	if document == nil || index == nil || index.root == nil {
		return nil
	}

	root, ok := resolveYAMLAlias(index.root)
	if !ok {
		return []Diagnostic{index.aliasResolutionDiagnostic("$")}
	}
	graph, keyNode, keyIssue := resolvedMappingValue(root, "graph")
	if keyIssue != yamlKeyIssueNone {
		return []Diagnostic{index.mappingKeyDiagnostic("$", keyNode, keyIssue)}
	}
	if graph == nil {
		return nil
	}
	graph, ok = resolveYAMLAlias(graph)
	if !ok {
		return []Diagnostic{index.aliasResolutionDiagnostic("graph")}
	}
	nodes, keyNode, keyIssue := resolvedMappingValue(graph, "nodes")
	if keyIssue != yamlKeyIssueNone {
		return []Diagnostic{index.mappingKeyDiagnostic("graph", keyNode, keyIssue)}
	}
	if nodes == nil {
		return nil
	}
	nodes, ok = resolveYAMLAlias(nodes)
	if !ok {
		return []Diagnostic{index.aliasResolutionDiagnostic("graph.nodes")}
	}
	if nodes.Kind != yaml.SequenceNode {
		return nil
	}

	var diagnostics []Diagnostic
	for nodeIndex, node := range document.Graph.Nodes {
		if nodeIndex >= len(nodes.Content) {
			break
		}
		var configPath string
		switch node.Type {
		case NodeSendMessage:
			configPath = "sendMessage"
		case NodeWait:
			configPath = "wait"
		case NodeRequestApproval:
			configPath = "requestApproval"
		default:
			continue
		}

		nodePath := fmt.Sprintf("graph.nodes[%d]", nodeIndex)
		sourceNode, ok := resolveYAMLAlias(nodes.Content[nodeIndex])
		if !ok {
			return append(diagnostics, index.aliasResolutionDiagnostic(nodePath))
		}
		config, keyNode, keyIssue := resolvedMappingValue(sourceNode, configPath)
		if keyIssue != yamlKeyIssueNone {
			return append(diagnostics, index.mappingKeyDiagnostic(nodePath, keyNode, keyIssue))
		}
		if config == nil {
			continue
		}
		config, ok = resolveYAMLAlias(config)
		if !ok {
			return append(diagnostics, index.aliasResolutionDiagnostic(nodePath+"."+configPath))
		}
		if node.Type == NodeSendMessage && node.SendMessage != nil {
			presence, terminal := sendMessageSourcePresenceDiagnostics(node, config, nodePath+"."+configPath, index)
			diagnostics = append(diagnostics, presence...)
			if terminal {
				return diagnostics
			}
		}
		if node.Type == NodeRequestApproval && node.RequestApproval != nil {
			presence, terminal := requestApprovalSourcePresenceDiagnostics(node, config, nodePath+"."+configPath, index)
			diagnostics = append(diagnostics, presence...)
			if terminal {
				return diagnostics
			}
		}

		path := nodePath + "." + configPath + ".retry"
		retry, keyNode, keyIssue := resolvedMappingValue(config, "retry")
		if keyIssue != yamlKeyIssueNone {
			return append(diagnostics, index.mappingKeyDiagnostic(nodePath+"."+configPath, keyNode, keyIssue))
		}
		if retry == nil {
			continue
		}
		retry, ok = resolveYAMLAlias(retry)
		if !ok {
			return append(diagnostics, index.aliasResolutionDiagnostic(path))
		}
		if retry.Tag != "!!null" {
			continue
		}
		diagnostics = append(diagnostics, index.decorate(Diagnostic{
			Severity: SeverityError,
			Code:     "syntax.type_mismatch",
			Path:     path,
			Message:  "retry must be an object when present; omit retry to use the compiler default",
			NodeID:   node.ID,
		}))
	}
	return diagnostics
}

func requestApprovalSourcePresenceDiagnostics(
	node Node,
	config *yaml.Node,
	path string,
	index *sourceIndex,
) ([]Diagnostic, bool) {
	value, keyNode, keyIssue := resolvedMappingValue(config, "feedbackScope")
	if keyIssue != yamlKeyIssueNone {
		return []Diagnostic{index.mappingKeyDiagnostic(path, keyNode, keyIssue)}, true
	}
	if value == nil {
		return nil, false
	}
	node.RequestApproval.feedbackScopePresent = true
	resolved, ok := resolveYAMLAlias(value)
	if !ok {
		return []Diagnostic{index.aliasResolutionDiagnostic(path + ".feedbackScope")}, true
	}
	if resolved.Tag == "!!null" {
		return []Diagnostic{{
			Severity: SeverityError,
			Code:     "syntax.type_mismatch",
			Path:     path + ".feedbackScope",
			Message:  "feedbackScope must be a value expression when present; omit feedbackScope to retain the legacy approval contract",
			NodeID:   node.ID,
		}}, true
	}
	return nil, false
}

func sendMessageSourcePresenceDiagnostics(
	node Node,
	config *yaml.Node,
	path string,
	index *sourceIndex,
) ([]Diagnostic, bool) {
	var diagnostics []Diagnostic
	for _, field := range []struct {
		name string
		mark func()
	}{
		{name: "workScope", mark: func() { node.SendMessage.workScopePresent = true }},
		{name: "relatedScopes", mark: func() { node.SendMessage.relatedScopesPresent = true }},
	} {
		value, keyNode, keyIssue := resolvedMappingValue(config, field.name)
		if keyIssue != yamlKeyIssueNone {
			return []Diagnostic{index.mappingKeyDiagnostic(path, keyNode, keyIssue)}, true
		}
		if value == nil {
			continue
		}
		field.mark()
		resolved, ok := resolveYAMLAlias(value)
		if !ok {
			return []Diagnostic{index.aliasResolutionDiagnostic(path + "." + field.name)}, true
		}
		if field.name == "relatedScopes" && resolved.Tag == "!!null" {
			diagnostics = append(diagnostics, index.decorate(Diagnostic{
				Severity: SeverityError,
				Code:     "syntax.type_mismatch",
				Path:     path + "." + field.name,
				Message:  "relatedScopes must be an array when present; use [] when there are no related scopes",
				NodeID:   node.ID,
			}))
		}
	}
	return diagnostics, false
}

func resolveYAMLAlias(node *yaml.Node) (*yaml.Node, bool) {
	seen := make(map[*yaml.Node]struct{})
	for depth := 0; node != nil && node.Kind == yaml.AliasNode; depth++ {
		if depth >= MaxSourceYAMLNesting {
			return nil, false
		}
		if _, exists := seen[node]; exists {
			return nil, false
		}
		seen[node] = struct{}{}
		node = node.Alias
	}
	return node, node != nil
}

type yamlKeyIssue uint8

const (
	yamlKeyIssueNone yamlKeyIssue = iota
	yamlKeyIssueAlias
	yamlKeyIssueUnsupported
)

func resolvedMappingValue(mapping *yaml.Node, key string) (*yaml.Node, *yaml.Node, yamlKeyIssue) {
	if mapping == nil || mapping.Kind != yaml.MappingNode {
		return nil, nil, yamlKeyIssueNone
	}
	for i := 0; i+1 < len(mapping.Content); i += 2 {
		sourceKey := mapping.Content[i]
		resolvedKey, ok := resolveYAMLAlias(sourceKey)
		if !ok {
			return nil, sourceKey, yamlKeyIssueAlias
		}
		if resolvedKey.Kind != yaml.ScalarNode || resolvedKey.Tag != "!!str" {
			return nil, sourceKey, yamlKeyIssueUnsupported
		}
		if resolvedKey.Value == key {
			return mapping.Content[i+1], nil, yamlKeyIssueNone
		}
	}
	return nil, nil, yamlKeyIssueNone
}

func (index *sourceIndex) mappingKeyDiagnostic(path string, key *yaml.Node, issue yamlKeyIssue) Diagnostic {
	if issue == yamlKeyIssueAlias {
		diagnostic := index.aliasResolutionDiagnostic(path)
		if key != nil {
			diagnostic.Line, diagnostic.Column = key.Line, key.Column
		}
		return diagnostic
	}
	diagnostic := Diagnostic{
		Severity: SeverityError,
		Code:     "syntax.unsupported_key",
		Path:     path,
		Message:  "YAML structural mapping keys must be strings; aliases may only resolve to string keys",
	}
	if key != nil {
		diagnostic.Line, diagnostic.Column = key.Line, key.Column
	}
	return index.decorate(diagnostic)
}

func (index *sourceIndex) aliasResolutionDiagnostic(path string) Diagnostic {
	return index.decorate(Diagnostic{
		Severity: SeverityError,
		Code:     "resource.yaml_alias_expansion",
		Path:     path,
		Message:  fmt.Sprintf("PlanVersion YAML aliases must be acyclic and no more than %d references deep", MaxSourceYAMLNesting),
	})
}

func yamlDiagnostics(err error, index *sourceIndex) []Diagnostic {
	var typeError *yaml.TypeError
	messages := []string{err.Error()}
	if errors.As(err, &typeError) {
		messages = typeError.Errors
	}
	diagnostics := make([]Diagnostic, 0, len(messages))
	for _, message := range messages {
		code := "syntax.invalid_yaml"
		if strings.Contains(message, "field ") && strings.Contains(message, "not found in type") {
			code = "syntax.unknown_field"
		} else if strings.Contains(message, "already defined") {
			code = "syntax.duplicate_key"
		} else if strings.Contains(message, "cannot unmarshal") {
			code = "syntax.type_mismatch"
		}
		line := extractYAMLLine(message)
		pos := position{path: "$", line: line}
		if index != nil {
			if candidate, ok := index.byLine[line]; ok {
				pos = candidate
			}
		}
		if pos.line > 0 && pos.column == 0 {
			pos.column = 1
		}
		diagnostic := Diagnostic{
			Severity: SeverityError,
			Code:     code,
			Path:     pos.path,
			Message:  message,
			Line:     pos.line,
			Column:   pos.column,
		}
		if index != nil {
			diagnostic = index.decorate(diagnostic)
		}
		diagnostics = append(diagnostics, diagnostic)
	}
	return diagnostics
}

func extractYAMLLine(message string) int {
	match := yamlLinePattern.FindStringSubmatch(message)
	if len(match) != 2 {
		return 0
	}
	line, _ := strconv.Atoi(match[1])
	return line
}

func indexSource(raw []byte) *sourceIndex {
	index := &sourceIndex{
		byPath: map[string]position{}, byLine: map[int]position{},
		nodes: map[int]string{}, edges: map[int]string{},
		merges: map[string]struct{}{},
	}
	var root yaml.Node
	if err := yaml.Unmarshal(raw, &root); err != nil || len(root.Content) == 0 {
		return index
	}
	index.root = root.Content[0]
	index.walk(index.root, "$", false)
	index.indexGraphIdentities(index.root)
	return index
}

func (index *sourceIndex) indexGraphIdentities(root *yaml.Node) {
	graph := mappingValue(root, "graph")
	if graph == nil {
		return
	}
	for i, node := range sequenceValue(graph, "nodes") {
		if id := mappingValue(node, "id"); id != nil {
			index.nodes[i] = id.Value
		}
	}
	for i, edge := range sequenceValue(graph, "edges") {
		if id := mappingValue(edge, "id"); id != nil {
			index.edges[i] = id.Value
		}
	}
}

func mappingValue(mapping *yaml.Node, key string) *yaml.Node {
	if mapping == nil || mapping.Kind != yaml.MappingNode {
		return nil
	}
	for i := 0; i+1 < len(mapping.Content); i += 2 {
		if mapping.Content[i].Value == key {
			return mapping.Content[i+1]
		}
	}
	return nil
}

func sequenceValue(mapping *yaml.Node, key string) []*yaml.Node {
	value := mappingValue(mapping, key)
	if value == nil || value.Kind != yaml.SequenceNode {
		return nil
	}
	return value.Content
}

func (index *sourceIndex) walk(node *yaml.Node, path string, useNodePosition bool) {
	if node == nil {
		return
	}
	if useNodePosition || path == "$" {
		pos := position{path: path, line: node.Line, column: node.Column}
		index.byPath[path] = pos
		if _, exists := index.byLine[node.Line]; !exists && node.Line > 0 {
			index.byLine[node.Line] = pos
		}
	}
	switch node.Kind {
	case yaml.AliasNode:
		// Keep position indexing proportional to the authored source. Alias
		// targets are resolved only for the bounded action.retry lookup above.
		return
	case yaml.MappingNode:
		for i := 0; i+1 < len(node.Content); i += 2 {
			key, value := node.Content[i], node.Content[i+1]
			resolvedKey, keyOK := resolveYAMLAlias(key)
			pathKey := key.Value
			if pathKey == "" {
				pathKey = fmt.Sprintf("<key@%d:%d>", key.Line, key.Column)
			}
			childPath := pathKey
			if path != "$" {
				childPath = path + "." + pathKey
			}
			switch {
			case keyOK && resolvedKey.Tag == "!!merge":
				index.merges[childPath] = struct{}{}
			}
			pos := position{path: childPath, line: key.Line, column: key.Column}
			index.byPath[childPath] = pos
			if _, exists := index.byLine[key.Line]; !exists {
				index.byLine[key.Line] = pos
			}
			index.walk(value, childPath, false)
		}
	case yaml.SequenceNode:
		for i, child := range node.Content {
			childPath := fmt.Sprintf("%s[%d]", path, i)
			index.walk(child, childPath, true)
		}
	}
}

func (index *sourceIndex) decorate(diagnostic Diagnostic) Diagnostic {
	if index == nil {
		return diagnostic
	}
	if diagnostic.Line == 0 {
		path := diagnostic.Path
		for path != "" {
			if pos, ok := index.byPath[path]; ok {
				diagnostic.Line, diagnostic.Column = pos.line, pos.column
				break
			}
			if bracket := strings.LastIndex(path, "["); bracket > strings.LastIndex(path, ".") {
				path = path[:bracket]
				continue
			}
			if dot := strings.LastIndex(path, "."); dot >= 0 {
				path = path[:dot]
				continue
			}
			break
		}
	}
	if diagnostic.NodeID == "" {
		if i, ok := indexedPath(diagnostic.Path, "graph.nodes["); ok {
			diagnostic.NodeID = index.nodes[i]
		}
	}
	if diagnostic.EdgeID == "" {
		if i, ok := indexedPath(diagnostic.Path, "graph.edges["); ok {
			diagnostic.EdgeID = index.edges[i]
		}
	}
	return diagnostic
}

func indexedPath(path, prefix string) (int, bool) {
	start := strings.Index(path, prefix)
	if start < 0 {
		return 0, false
	}
	start += len(prefix)
	end := strings.IndexByte(path[start:], ']')
	if end < 0 {
		return 0, false
	}
	index, err := strconv.Atoi(path[start : start+end])
	return index, err == nil
}
