package state

import (
	"fmt"
	"regexp"
	"slices"
	"strings"
	"unicode"

	"github.com/devrites/devrites/internal/markdowntext"
)

// TaskSlice is one SLICE-### block parsed from tasks.md. Via maps a dependency
// ID to the interface its `(via <text>)` suffix names; bare edges are absent.
type TaskSlice struct {
	ID               string
	Dependencies     []string
	Via              map[string]string
	ConsumesProduces string
}

// TaskGraphResult is the deterministic outcome of parsing a tasks.md slice graph.
type TaskGraphResult struct {
	WorkflowOnly bool // explicit work scope, never inferred from an empty graph
	Slices       []TaskSlice
	Cycle        []string // empty when acyclic
	Unknown      []string // dependency ids with no defining slice
	Problems     []string // human-readable blockers
}

var (
	sliceHeaderRE  = regexp.MustCompile(`(?m)^##\s+(SLICE-\d+)\b`)
	anyH2HeaderRE  = regexp.MustCompile(`(?m)^##\s`)
	dependenciesRE = regexp.MustCompile(`(?m)^Dependencies:[ \t]*(.+)[ \t]*$`)
	dependsOnRE    = regexp.MustCompile(`(?m)^depends_on:[ \t]*(.+)[ \t]*$`)
	consumesRE     = regexp.MustCompile(`(?m)^Consumes / Produces:[ \t]*(.+)[ \t]*$`)
	// viaSuffixRE matches a Dependencies `(via <text>)` suffix; group 2 is
	// empty when the parenthetical is unclosed. Nested parentheses are unsupported.
	viaSuffixRE    = regexp.MustCompile(`\([ \t]*via\b([^()]*)(\))?`)
	sliceIDValidRE = regexp.MustCompile(`^SLICE-\d+$`)
)

// ValidSliceID reports whether id has the canonical SLICE-### shape.
func ValidSliceID(id string) bool { return sliceIDValidRE.MatchString(id) }

// ExtractTaskSlice returns the first `## SLICE-###` section of tasks.md whose ID
// equals id, from its heading up to the next level-2 heading. It lets a phase read
// one slice instead of the whole file.
func ExtractTaskSlice(tasksMarkdown []byte, id string) (string, bool) {
	text := string(tasksMarkdown)
	masked := structuralTaskText(tasksMarkdown)
	for _, match := range sliceHeaderRE.FindAllStringSubmatchIndex(masked, -1) {
		if masked[match[2]:match[3]] != id {
			continue
		}
		rest := masked[match[1]:]
		end := len(text)
		if next := anyH2HeaderRE.FindStringIndex(rest); next != nil {
			end = match[1] + next[0]
		}
		return strings.TrimRight(text[match[0]:end], "\n") + "\n", true
	}
	return "", false
}

// ParseTaskGraph reads tasks.md content and validates the slice dependency DAG.
func ParseTaskGraph(tasksMarkdown []byte) TaskGraphResult {
	text := structuralTaskText(tasksMarkdown)
	result := TaskGraphResult{}
	if strings.TrimSpace(text) == "" {
		result.Problems = append(result.Problems, "tasks.md is empty")
		return result
	}

	headers := sliceHeaderRE.FindAllStringSubmatchIndex(text, -1)
	markers := 0
	for _, line := range strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n") {
		if line == "Work scope: workflow-only" {
			markers++
		}
	}
	if markers > 0 {
		if markers != 1 || len(headers) != 0 {
			result.Problems = append(result.Problems, "workflow-only scope requires exactly one marker and zero SLICE-### sections")
			return result
		}
		result.WorkflowOnly = true
		return result
	}
	if len(headers) == 0 {
		result.Problems = append(result.Problems, "no SLICE-### sections found")
		return result
	}

	known := make(map[string]bool, len(headers))
	firstIndex := make(map[string]int, len(headers))
	for i, match := range headers {
		id := text[match[2]:match[3]]
		if _, seen := firstIndex[id]; seen {
			result.Problems = append(result.Problems, fmt.Sprintf("duplicate slice id %s", id))
			continue
		}
		firstIndex[id] = i
		known[id] = true
	}

	for i, match := range headers {
		id := text[match[2]:match[3]]
		if firstIndex[id] != i {
			continue
		}
		start := match[1]
		end := len(text)
		if i+1 < len(headers) {
			end = headers[i+1][0]
		}
		block := text[start:end]
		slice, problems := parseSliceGraph(id, block)
		result.Problems = append(result.Problems, problems...)
		result.Slices = append(result.Slices, slice)
		for _, dep := range slice.Dependencies {
			if !known[dep] {
				result.Unknown = append(result.Unknown, dep)
				result.Problems = append(result.Problems, fmt.Sprintf("%s depends on unknown slice %s", id, dep))
			}
		}
	}

	if cycle := findTaskCycle(result.Slices); len(cycle) > 0 {
		result.Cycle = cycle
		result.Problems = append(result.Problems, "dependency cycle: "+strings.Join(cycle, " -> "))
	}
	return result
}

// structuralTaskText blanks fenced code blocks, keeping byte offsets aligned, so
// an example inside a fence is never read as slice structure. Input that cannot
// be masked (malformed UTF-8, NUL) is parsed as written.
func structuralTaskText(tasksMarkdown []byte) string {
	masked, err := markdowntext.Structural(tasksMarkdown)
	if err != nil {
		return string(tasksMarkdown)
	}
	return string(masked)
}

func parseSliceGraph(id, block string) (TaskSlice, []string) {
	var problems []string
	depsLine := firstLineValue(dependenciesRE, block)
	mirrorLine := firstLineValue(dependsOnRE, block)
	bare, via, malformed := splitDependencyVia(depsLine)
	deps, malformedIDs := parseIDList(bare)
	for _, token := range append(malformed, malformedIDs...) {
		problems = append(problems, fmt.Sprintf("%s has malformed dependency %q", id, token))
	}
	for dep := range via {
		if !slices.Contains(deps, dep) {
			delete(via, dep) // its ID token is already reported malformed
		}
	}
	mirror, mirrorMalformed := parseIDList(mirrorLine)
	for _, token := range mirrorMalformed {
		problems = append(problems, fmt.Sprintf("%s has malformed depends_on %q", id, token))
	}
	if depsLine != "" && mirrorLine != "" && !sameIDSet(deps, mirror) {
		problems = append(problems, fmt.Sprintf("%s Dependencies and depends_on sets differ", id))
	}
	if depsLine == "" && mirrorLine == "" {
		problems = append(problems, fmt.Sprintf("%s is missing Dependencies", id))
	}
	if depsLine == "" {
		deps = mirror
	}
	return TaskSlice{ID: id, Dependencies: deps, Via: via, ConsumesProduces: firstLineValue(consumesRE, block)}, problems
}

// splitDependencyVia strips each `(via <text>)` suffix from a Dependencies
// value, returning the bare ID list, the via text keyed by the ID it follows,
// and any suffix that is empty, unclosed, or not directly after an ID.
func splitDependencyVia(raw string) (string, map[string]string, []string) {
	var bare strings.Builder
	var via map[string]string
	var malformed []string
	last := 0
	for _, m := range viaSuffixRE.FindAllStringSubmatchIndex(raw, -1) {
		before := raw[last:m[0]]
		bare.WriteString(before)
		last = m[1]
		text := strings.TrimSpace(raw[m[2]:m[3]])
		dep := precedingSliceID(before)
		if m[4] < 0 || text == "" || dep == "" {
			malformed = append(malformed, strings.TrimSpace(raw[m[0]:m[1]]))
			continue
		}
		if via == nil {
			via = make(map[string]string)
		}
		if _, seen := via[dep]; !seen {
			via[dep] = text
		}
	}
	bare.WriteString(raw[last:])
	return bare.String(), via, malformed
}

// precedingSliceID returns the SLICE-### token that ends s, separated from what
// follows only by blanks, or "" when s ends in a separator or a non-ID token.
func precedingSliceID(s string) string {
	s = strings.TrimRight(s, " \t")
	start := strings.LastIndexAny(s, ",; \t") + 1
	id := strings.Trim(s[start:], "`'\"(){}[]")
	if !sliceIDValidRE.MatchString(id) {
		return ""
	}
	return id
}

func firstLineValue(re *regexp.Regexp, block string) string {
	match := re.FindStringSubmatch(block)
	if len(match) < 2 {
		return ""
	}
	return strings.TrimSpace(match[1])
}

func parseIDList(raw string) ([]string, []string) {
	raw = strings.TrimSpace(raw)
	raw = strings.Trim(raw, "[]")
	raw = strings.TrimSpace(raw)
	if raw == "" || strings.EqualFold(raw, "none") {
		return nil, nil
	}
	parts := strings.FieldsFunc(raw, func(r rune) bool {
		return r == ',' || r == ';' || unicode.IsSpace(r)
	})
	var ids []string
	var malformed []string
	seen := make(map[string]bool)
	for _, part := range parts {
		id := strings.Trim(strings.TrimSpace(part), "`'\"(){}")
		if id == "" {
			continue
		}
		if !sliceIDValidRE.MatchString(id) {
			malformed = append(malformed, id)
			continue
		}
		if seen[id] {
			continue
		}
		seen[id] = true
		ids = append(ids, id)
	}
	return ids, malformed
}

func sameIDSet(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	seen := make(map[string]bool, len(a))
	for _, id := range a {
		seen[id] = true
	}
	for _, id := range b {
		if !seen[id] {
			return false
		}
	}
	return true
}

func findTaskCycle(slices []TaskSlice) []string {
	graph := make(map[string][]string, len(slices))
	for _, slice := range slices {
		graph[slice.ID] = append([]string(nil), slice.Dependencies...)
	}
	visited := make(map[string]uint8, len(slices))
	stack := make([]string, 0, len(slices))
	var cycle []string

	var visit func(id string) bool
	visit = func(id string) bool {
		switch visited[id] {
		case 2:
			return false
		case 1:
			for i := len(stack) - 1; i >= 0; i-- {
				cycle = append(cycle, stack[i])
				if stack[i] == id {
					break
				}
			}
			for i, j := 0, len(cycle)-1; i < j; i, j = i+1, j-1 {
				cycle[i], cycle[j] = cycle[j], cycle[i]
			}
			return true
		}
		visited[id] = 1
		stack = append(stack, id)
		for _, dep := range graph[id] {
			if visit(dep) {
				return true
			}
		}
		stack = stack[:len(stack)-1]
		visited[id] = 2
		return false
	}

	for _, slice := range slices {
		if visited[slice.ID] == 0 {
			cycle = nil
			if visit(slice.ID) {
				return cycle
			}
		}
	}
	return nil
}
