package shellsafe

import (
	"sort"
	"strings"

	"mvdan.cc/sh/v3/syntax"

	"reasonix/internal/shellparse"
)

// NormalizeBashSafeRedirectsForMatch returns a copy of subject with redirect
// syntax removed only when the redirect cannot write to a real file. It is used
// for shell safety matching, never for execution.
//
// Supported safe forms are fd duplication/close (`2>&1`, `>&2`, `2>&-`,
// `0<&1`) and output redirects to a null sink (`>/dev/null`, `>$null`,
// `>nul`, `2> /dev/null`, `>>/dev/null`, `&>/dev/null`, `&>>/dev/null`).
// The shell execution layer normalizes these null-sink spellings to the actual
// sink for the resolved shell. Other redirections are left unnormalized so the
// usual shell-syntax guard keeps prefix/read-only matching conservative.
func NormalizeBashSafeRedirectsForMatch(subject string) (string, bool) {
	file, err := shellparse.ParseBash(subject)
	if err != nil || shellparse.HasHereDoc(file) {
		return "", false
	}
	spans, ok := safeRedirectSpans(subject, file.Stmts)
	if !ok {
		return "", false
	}
	if len(spans) == 0 {
		return subject, true
	}
	sort.Slice(spans, func(i, j int) bool { return spans[i].start < spans[j].start })

	var out strings.Builder
	last := 0
	for _, span := range spans {
		if span.start < last || span.end > len(subject) {
			return "", false
		}
		out.WriteString(subject[last:span.start])
		last = span.end
	}
	out.WriteString(subject[last:])
	return strings.TrimSpace(out.String()), true
}

type redirectSpan struct {
	start int
	end   int
}

func safeRedirectSpans(source string, stmts []*syntax.Stmt) ([]redirectSpan, bool) {
	var spans []redirectSpan
	for _, stmt := range stmts {
		if !appendSafeRedirectSpans(source, stmt, &spans) {
			return nil, false
		}
	}
	return spans, true
}

func appendSafeRedirectSpans(source string, stmt *syntax.Stmt, spans *[]redirectSpan) bool {
	if stmt == nil {
		return true
	}
	for _, redir := range stmt.Redirs {
		span, ok := safeRedirectSpan(source, redir)
		if !ok {
			return false
		}
		*spans = append(*spans, span)
	}
	if binary, ok := stmt.Cmd.(*syntax.BinaryCmd); ok {
		return appendSafeRedirectSpans(source, binary.X, spans) &&
			appendSafeRedirectSpans(source, binary.Y, spans)
	}
	return true
}

func safeRedirectSpan(source string, redir *syntax.Redirect) (redirectSpan, bool) {
	if redir == nil {
		return redirectSpan{}, false
	}
	switch redir.Op {
	case syntax.DplOut, syntax.DplIn:
		if !isSafeFDDupWord(source, redir.Word) {
			return redirectSpan{}, false
		}
	case syntax.RdrOut, syntax.AppOut, syntax.RdrClob, syntax.AppClob, syntax.RdrAll, syntax.AppAll, syntax.RdrAllClob, syntax.AppAllClob:
		if !isNullRedirectWord(source, redir.Word) {
			return redirectSpan{}, false
		}
	default:
		return redirectSpan{}, false
	}
	start := int(redir.OpPos.Offset())
	if redir.N != nil && redir.N.Pos().IsValid() {
		start = int(redir.N.Pos().Offset())
	}
	end := int(redir.End().Offset())
	if start < 0 || end < start || end > len(source) {
		return redirectSpan{}, false
	}
	return redirectSpan{start: start, end: end}, true
}

func isSafeFDDupWord(source string, word *syntax.Word) bool {
	value := redirectWordSource(source, word)
	if value == "-" {
		return true
	}
	if value == "" {
		return false
	}
	for i := range len(value) {
		if value[i] < '0' || value[i] > '9' {
			return false
		}
	}
	return true
}

func isNullRedirectWord(source string, word *syntax.Word) bool {
	value := redirectWordSource(source, word)
	if value == "/dev/null" {
		return true
	}
	return strings.EqualFold(value, "$null") || strings.EqualFold(value, "nul")
}

func redirectWordSource(source string, word *syntax.Word) string {
	if word == nil || !word.Pos().IsValid() || !word.End().IsValid() {
		return ""
	}
	start := int(word.Pos().Offset())
	end := int(word.End().Offset())
	if start < 0 || end < start || end > len(source) {
		return ""
	}
	return strings.TrimSpace(source[start:end])
}
