package core

import (
	"html"
	"regexp"
	"strings"

	"golang.org/x/net/html/atom"

	ghtml "golang.org/x/net/html"
)

// skipAtoms are tags whose entire subtree is discarded.
var skipAtoms = map[atom.Atom]bool{
	atom.Script:   true,
	atom.Style:    true,
	atom.Noscript: true,
}

// HtmlToMarkdown converts an HTML string to readable markdown (best-effort).
//
// Supported elements (in priority order):
//   - <h1>..<h6>  → ATX headings (#..######)
//   - <p>         → paragraph separated by blank line
//   - <a href>    → [text](href)
//   - <strong>,<b> → **text**
//   - <em>,<i>    → *text*
//   - <code>      → `text`
//   - <pre>       → fenced code block
//   - <ul>/<ol>/<li> → bullet or numbered list
//   - <br>        → newline
//   - <hr>        → ---
//   - <img alt src> → ![alt](src)
//   - <blockquote> → > text
//
// Skipped: <script>, <style>, <noscript> (and their subtrees).
// Everything else (div, span, section, etc.) emits descendant text without markup.
// Multiple whitespace in text nodes is collapsed to a single space.
// HTML entities are decoded via html.UnescapeString.
func HtmlToMarkdown(raw string) string {
	doc, err := ghtml.Parse(strings.NewReader(raw))
	if err != nil {
		// If parsing fails, return the raw string stripped of tags as a fallback.
		return stripTags(raw)
	}

	var b strings.Builder
	walkNode(&b, doc, &walkState{})

	// Collapse 3+ consecutive newlines → 2.
	result := collapseNewlines(b.String())
	return strings.TrimSpace(result)
}

// walkState carries context through the recursive walk.
type walkState struct {
	inPre       bool
	listStack   []atom.Atom // ul or ol
	blockquote  int         // nesting depth
	skipDepth   int         // subtree skip depth
}

func walkNode(b *strings.Builder, n *ghtml.Node, s *walkState) {
	switch n.Type {
	case ghtml.CommentNode:
		return // skip HTML comments

	case ghtml.TextNode:
		if s.skipDepth > 0 {
			return
		}
		text := html.UnescapeString(n.Data)
		if s.inPre {
			b.WriteString(text)
		} else {
			// Collapse whitespace sequences to a single space.
			text = collapseWhitespace(text)
			if text != "" {
				b.WriteString(text)
			}
		}
		return

	case ghtml.ElementNode:
		a := n.DataAtom
		if skipAtoms[a] {
			s.skipDepth++
			walkChildren(b, n, s)
			s.skipDepth--
			return
		}
		if s.skipDepth > 0 {
			walkChildren(b, n, s)
			return
		}
		renderElement(b, n, s, a)
		return
	}

	// For document, doctype, etc.: just walk children.
	walkChildren(b, n, s)
}

func renderElement(b *strings.Builder, n *ghtml.Node, s *walkState, a atom.Atom) {
	switch a {
	case atom.H1, atom.H2, atom.H3, atom.H4, atom.H5, atom.H6:
		level := headingLevel(a)
		prefix := strings.Repeat("#", level) + " "
		b.WriteString("\n\n" + prefix)
		walkChildren(b, n, s)
		b.WriteString("\n\n")

	case atom.P:
		b.WriteString("\n\n")
		walkChildren(b, n, s)
		b.WriteString("\n\n")

	case atom.Br:
		b.WriteString("\n")

	case atom.Hr:
		b.WriteString("\n\n---\n\n")

	case atom.Pre:
		s.inPre = true
		b.WriteString("\n\n```\n")
		walkChildren(b, n, s)
		b.WriteString("\n```\n\n")
		s.inPre = false

	case atom.Code:
		if s.inPre {
			walkChildren(b, n, s)
		} else {
			b.WriteString("`")
			walkChildren(b, n, s)
			b.WriteString("`")
		}

	case atom.Strong, atom.B:
		b.WriteString("**")
		walkChildren(b, n, s)
		b.WriteString("**")

	case atom.Em, atom.I:
		b.WriteString("*")
		walkChildren(b, n, s)
		b.WriteString("*")

	case atom.A:
		href := attrVal(n, "href")
		var text strings.Builder
		walkChildren(&text, n, s)
		linkText := strings.TrimSpace(text.String())
		if linkText == "" {
			linkText = href
		}
		b.WriteString("[" + linkText + "](" + href + ")")

	case atom.Img:
		src := attrVal(n, "src")
		if src == "" {
			src = attrVal(n, "data-src")
		}
		alt := attrVal(n, "alt")
		b.WriteString("![" + alt + "](" + src + ")")

	case atom.Ul:
		s.listStack = append(s.listStack, atom.Ul)
		b.WriteString("\n")
		walkChildren(b, n, s)
		b.WriteString("\n")
		s.listStack = s.listStack[:len(s.listStack)-1]

	case atom.Ol:
		s.listStack = append(s.listStack, atom.Ol)
		b.WriteString("\n")
		walkChildren(b, n, s)
		b.WriteString("\n")
		s.listStack = s.listStack[:len(s.listStack)-1]

	case atom.Li:
		prefix := "- "
		if len(s.listStack) > 0 && s.listStack[len(s.listStack)-1] == atom.Ol {
			prefix = "1. "
		}
		b.WriteString("\n" + prefix)
		walkChildren(b, n, s)

	case atom.Blockquote:
		s.blockquote++
		b.WriteString("\n\n")
		// Capture children into a temp buffer then prefix each line with "> ".
		var inner strings.Builder
		walkChildren(&inner, n, s)
		s.blockquote--
		quoted := prefixLines(strings.TrimSpace(inner.String()), "> ")
		b.WriteString(quoted)
		b.WriteString("\n\n")

	default:
		// div, span, section, article, header, footer, nav, etc.
		// Just emit descendant text without any markup.
		walkChildren(b, n, s)
	}
}

func walkChildren(b interface{ WriteString(string) (int, error) }, n *ghtml.Node, s *walkState) {
	// We accept a strings.Builder-like writer.
	sb, ok := b.(*strings.Builder)
	if !ok {
		return
	}
	for c := n.FirstChild; c != nil; c = c.NextSibling {
		walkNode(sb, c, s)
	}
}

// headingLevel maps h1..h6 atom to 1..6.
func headingLevel(a atom.Atom) int {
	switch a {
	case atom.H1:
		return 1
	case atom.H2:
		return 2
	case atom.H3:
		return 3
	case atom.H4:
		return 4
	case atom.H5:
		return 5
	case atom.H6:
		return 6
	}
	return 1
}

// attrVal returns the value of the named attribute, or "".
func attrVal(n *ghtml.Node, name string) string {
	for _, a := range n.Attr {
		if a.Key == name {
			return a.Val
		}
	}
	return ""
}

var wsRe = regexp.MustCompile(`[ \t\r\n]+`)

// collapseWhitespace reduces any run of whitespace to a single space.
func collapseWhitespace(s string) string {
	return wsRe.ReplaceAllString(s, " ")
}

var manyNL = regexp.MustCompile(`\n{3,}`)

// collapseNewlines reduces 3+ consecutive newlines to 2.
func collapseNewlines(s string) string {
	return manyNL.ReplaceAllString(s, "\n\n")
}

// prefixLines prepends prefix to every non-empty line in s.
func prefixLines(s, prefix string) string {
	lines := strings.Split(s, "\n")
	var out []string
	for _, l := range lines {
		if strings.TrimSpace(l) == "" {
			out = append(out, "")
		} else {
			out = append(out, prefix+l)
		}
	}
	return strings.Join(out, "\n")
}

var tagRe = regexp.MustCompile(`<[^>]+>`)

// stripTags is a last-resort fallback: remove all HTML tags.
func stripTags(s string) string {
	return tagRe.ReplaceAllString(s, "")
}
