// Package chat implements messaging primitives. sanitize.go is the
// server-side rich-text whitelist sanitizer (§2.2): allowed tags are
// b i u s a code pre ul ol li br p blockquote; only <a> keeps an attribute
// (href, http/https/mailto only). Everything else is escaped as text.
// Implemented with a small hand-written tokenizer — no parser dependency.
package chat

import (
	"html"
	"strings"
)

var allowedTags = map[string]bool{
	"b": true, "i": true, "u": true, "s": true, "a": true, "code": true,
	"pre": true, "ul": true, "ol": true, "li": true, "br": true, "p": true,
	"blockquote": true,
}

var voidTags = map[string]bool{"br": true}

// SanitizeHTML rewrites untrusted rich text into safe HTML. Unknown tags are
// dropped (their text content kept), attributes are stripped (except a safe
// href on <a>), tags are re-balanced so output never leaks open elements.
func SanitizeHTML(in string) string {
	var out strings.Builder
	var stack []string

	i := 0
	for i < len(in) {
		c := in[i]
		if c != '<' {
			j := strings.IndexByte(in[i:], '<')
			if j < 0 {
				out.WriteString(html.EscapeString(in[i:]))
				break
			}
			out.WriteString(html.EscapeString(in[i : i+j]))
			i += j
			continue
		}
		// find the end of the tag
		end := strings.IndexByte(in[i:], '>')
		if end < 0 {
			out.WriteString(html.EscapeString(in[i:]))
			break
		}
		raw := in[i+1 : i+end] // between < and >
		i += end + 1

		closing := strings.HasPrefix(raw, "/")
		raw = strings.TrimPrefix(raw, "/")
		raw = strings.TrimSuffix(strings.TrimSpace(raw), "/")
		name, attrs := splitTag(raw)
		name = strings.ToLower(name)

		if !allowedTags[name] {
			continue // drop the tag entirely, keep surrounding text
		}
		if closing {
			// close up to the matching open tag, if any
			for n := len(stack) - 1; n >= 0; n-- {
				if stack[n] == name {
					for len(stack) > n {
						top := stack[len(stack)-1]
						stack = stack[:len(stack)-1]
						out.WriteString("</" + top + ">")
					}
					break
				}
			}
			continue
		}
		if voidTags[name] {
			out.WriteString("<" + name + ">")
			continue
		}
		if name == "a" {
			href := safeHref(attrs)
			if href == "" {
				out.WriteString("<a>")
			} else {
				out.WriteString(`<a href="` + html.EscapeString(href) + `" rel="noopener noreferrer" target="_blank">`)
			}
		} else {
			out.WriteString("<" + name + ">")
		}
		stack = append(stack, name)
	}
	// balance anything left open
	for n := len(stack) - 1; n >= 0; n-- {
		out.WriteString("</" + stack[n] + ">")
	}
	return out.String()
}

// splitTag separates the tag name from its raw attribute string.
func splitTag(raw string) (string, string) {
	for i := 0; i < len(raw); i++ {
		switch raw[i] {
		case ' ', '\t', '\n', '\r':
			return raw[:i], raw[i+1:]
		}
	}
	return raw, ""
}

// safeHref extracts href and accepts only http, https, or mailto schemes.
func safeHref(attrs string) string {
	lower := strings.ToLower(attrs)
	idx := strings.Index(lower, "href")
	if idx < 0 {
		return ""
	}
	rest := strings.TrimSpace(attrs[idx+4:])
	if !strings.HasPrefix(rest, "=") {
		return ""
	}
	rest = strings.TrimSpace(rest[1:])
	var val string
	switch {
	case strings.HasPrefix(rest, `"`):
		if end := strings.IndexByte(rest[1:], '"'); end >= 0 {
			val = rest[1 : 1+end]
		}
	case strings.HasPrefix(rest, "'"):
		if end := strings.IndexByte(rest[1:], '\''); end >= 0 {
			val = rest[1 : 1+end]
		}
	default:
		val = rest
		if sp := strings.IndexAny(val, " \t\n"); sp >= 0 {
			val = val[:sp]
		}
	}
	val = strings.TrimSpace(val)
	low := strings.ToLower(val)
	if strings.HasPrefix(low, "http://") || strings.HasPrefix(low, "https://") ||
		strings.HasPrefix(low, "mailto:") {
		return val
	}
	return ""
}

// SanitizePlain strips ALL tags, returning escaped plain text (used where
// rich text is not allowed).
func SanitizePlain(in string) string {
	var out strings.Builder
	i := 0
	for i < len(in) {
		if in[i] != '<' {
			j := strings.IndexByte(in[i:], '<')
			if j < 0 {
				out.WriteString(html.EscapeString(in[i:]))
				break
			}
			out.WriteString(html.EscapeString(in[i : i+j]))
			i += j
			continue
		}
		end := strings.IndexByte(in[i:], '>')
		if end < 0 {
			out.WriteString(html.EscapeString(in[i:]))
			break
		}
		i += end + 1
	}
	return out.String()
}
