// markdown.go — minimal Markdown → safe HTML converter for broadcast emails.
//
// Paliad doesn't pull in a third-party Markdown library — the body subset
// senders need is small and predictable, so we render it inline. Inputs are
// HTML-escaped first; the renderer then re-introduces a small whitelist of
// inline tags (<strong>, <em>, <code>, <a>) and block elements (<p>, <ul>,
// <li>, <br>) for the patterns it recognises. Anything we don't recognise
// stays escaped, so an attacker who tries to slip a <script> tag through
// the compose modal sees a literal "&lt;script&gt;" in the rendered email.
//
// Supported syntax:
//   - Paragraphs separated by blank lines.
//   - Single line break inside a paragraph → <br>.
//   - **bold** → <strong>bold</strong>
//   - _italic_ or *italic* → <em>italic</em>
//   - `inline code` → <code>inline code</code>
//   - [text](https://link) → <a href="...">text</a>
//   - Lines starting with "- " or "* " → <ul><li>...</li></ul>
//
// Out-of-scope (intentional, per t-paliad-147 v1):
//   - Headings, blockquotes, ordered lists, fenced code blocks, images,
//     tables. These can be added on demand without changing the contract.
package services

import (
	"fmt"
	"html"
	"regexp"
	"strings"
)

// renderMarkdownSafe converts Markdown to HTML. Output is safe for direct
// embedding in an HTML email body: every byte of input is escaped before
// the markdown post-processor runs, and the inline rewriter only re-emits
// a small whitelist of tags.
func renderMarkdownSafe(src string) string {
	src = strings.ReplaceAll(src, "\r\n", "\n")
	src = strings.ReplaceAll(src, "\r", "\n")

	// Split into paragraphs on blank lines.
	paragraphs := strings.Split(src, "\n\n")
	var out strings.Builder
	for _, raw := range paragraphs {
		p := strings.TrimSpace(raw)
		if p == "" {
			continue
		}
		// Bullet lists: every line starts with "- " or "* ".
		if isBulletList(p) {
			out.WriteString("<ul>\n")
			for _, line := range strings.Split(p, "\n") {
				item := strings.TrimSpace(line)
				if len(item) >= 2 && (item[:2] == "- " || item[:2] == "* ") {
					item = strings.TrimSpace(item[2:])
				}
				out.WriteString("  <li>")
				out.WriteString(renderInline(item))
				out.WriteString("</li>\n")
			}
			out.WriteString("</ul>\n")
			continue
		}

		// Plain paragraph. Single-newline within → <br>.
		lines := strings.Split(p, "\n")
		out.WriteString("<p>")
		for i, line := range lines {
			if i > 0 {
				out.WriteString("<br>\n")
			}
			out.WriteString(renderInline(strings.TrimSpace(line)))
		}
		out.WriteString("</p>\n")
	}
	return out.String()
}

func isBulletList(p string) bool {
	for _, line := range strings.Split(p, "\n") {
		t := strings.TrimSpace(line)
		if len(t) < 2 {
			return false
		}
		if t[:2] != "- " && t[:2] != "* " {
			return false
		}
	}
	return true
}

var (
	mdLinkRE   = regexp.MustCompile(`\[([^\]]+)\]\((https?://[^\s)]+)\)`)
	mdBoldRE   = regexp.MustCompile(`\*\*([^*]+)\*\*`)
	mdItalRE1  = regexp.MustCompile(`(^|[^\w])_([^_]+)_($|[^\w])`)
	mdItalRE2  = regexp.MustCompile(`(^|[^\w*])\*([^*]+)\*($|[^\w*])`)
	mdCodeRE   = regexp.MustCompile("`([^`]+)`")
)

// renderInline applies inline markdown to one line. The input is escaped
// first; replacements re-emit whitelisted tags.
func renderInline(line string) string {
	s := html.EscapeString(line)
	// Order matters: links first (they wrap text+URL), then bold (which is
	// **…** and would otherwise be split by the italic *…* rule), then
	// italics, then code.
	s = mdLinkRE.ReplaceAllStringFunc(s, func(m string) string {
		matches := mdLinkRE.FindStringSubmatch(m)
		if len(matches) != 3 {
			return m
		}
		text, url := matches[1], matches[2]
		// URL is already escaped by html.EscapeString above; href quoting
		// also needs the &-form so screen readers don't choke.
		return fmt.Sprintf(`<a href="%s">%s</a>`, url, text)
	})
	s = mdBoldRE.ReplaceAllString(s, `<strong>$1</strong>`)
	s = mdItalRE1.ReplaceAllString(s, `$1<em>$2</em>$3`)
	s = mdItalRE2.ReplaceAllString(s, `$1<em>$2</em>$3`)
	s = mdCodeRE.ReplaceAllString(s, `<code>$1</code>`)
	return s
}

// escapeHTML is a thin alias used by senderSignature so the broadcast file
// doesn't need to import html directly.
func escapeHTML(s string) string {
	return html.EscapeString(s)
}
