// Package parser extracts title, description, text content, and links from HTML.
// parser 包负责 HTML 解析：从网页 HTML 中提取标题、描述、正文和所有超链接。
package parser

import (
	"net/url"                           // URL 解析（规范化）
	"path"                              // 路径处理（提取目录、规范化相对路径）
	"regexp"                            // 正则表达式（空白字符替换）
	"sort"                              // query 参数排序（URL 规范化去重）
	"strings"                           // 字符串操作

	"golang.org/x/net/html"             // 标准 HTML 解析器（将 HTML 解析为 DOM 树）
)

// wsRe 空白字符正则：将任意连续空白字符（空格、换行、制表符等）替换为单个空格。
var wsRe = regexp.MustCompile(`\s+`)

// ParseHTML 解析 HTML 文档，返回标题、meta 描述、正文文本和所有超链接列表。
// body：原始 HTML 字符串；baseURL：用于解析相对链接的基准 URL。
func ParseHTML(body, baseURL string) (title, description, text string, hrefs []string) {
	// 从 baseURL 提取基准协议和主机（如 "https://example.com"）
	base := baseFromURL(baseURL)
	// 从 baseURL 提取当前页面路径（如 "/path/page.html"）
	basePath := pathFromURL(baseURL)

	// 将 HTML 字符串解析为 DOM 树
	doc, err := html.Parse(strings.NewReader(body))
	if err != nil {
		return // 解析失败返回空
	}

	var textParts []string // 收集所有正文文本片段

	// 深度优先遍历 DOM 树
	var dfs func(n *html.Node)
	dfs = func(n *html.Node) {
		if n.Type == html.ElementNode {
			tag := strings.ToLower(n.Data)

			// 跳过 <script>、<style>、<svg> 等无需解析内容的标签
			if tag == "script" || tag == "style" || tag == "svg" {
				return
			}

			// 提取 <meta name="description" content="..."> 标签
			if tag == "meta" {
				name := ""
				content := ""
				for _, a := range n.Attr {
					switch strings.ToLower(a.Key) {
					case "name":
						name = strings.ToLower(a.Val)
					case "content":
						content = a.Val
					}
				}
				// 只取第一个描述
				if name == "description" && description == "" {
					description = content
				}
			}

			// 提取 <a href="..."> 链接
			if tag == "a" {
				href := attrVal(n, "href")
				if href != "" && isSafeURL(href) {
					href = strings.SplitN(href, "#", 2)[0]
					if href != "" {
						href = resolveURL(base, basePath, href)
						if href != "" {
							href = NormalizeURL(href)
							if href != "" {
								hrefs = append(hrefs, href)
							}
						}
					}
				}
			}
		}

		// 提取文本节点（<title> 和正文内容）
		if n.Type == html.TextNode && n.Parent != nil {
			parentTag := ""
			if n.Parent.Type == html.ElementNode {
				parentTag = strings.ToLower(n.Parent.Data)
			}
			// 跳过 script/style/svg 内的文本
			if parentTag == "script" || parentTag == "style" || parentTag == "svg" {
				goto children
			}
			// 空白压缩并去除首尾空格
			s := wsRe.ReplaceAllString(n.Data, " ")
			s = strings.TrimSpace(s)
			if s != "" {
				if parentTag == "title" {
					title = s // 标题只取第一个
				} else {
					textParts = append(textParts, s) // 正文片段收集
				}
			}
		}

	children:
		// 递归遍历子节点
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			dfs(c)
		}
	}
	dfs(doc)

	// 将正文片段用空格连接为完整文本
	text = strings.Join(textParts, " ")
	return
}

// attrVal 提取 HTML 节点上指定名称的属性值（不区分大小写）。
func attrVal(n *html.Node, key string) string {
	for _, a := range n.Attr {
		if strings.ToLower(a.Key) == key {
			return a.Val
		}
	}
	return ""
}

// baseFromURL 从原始 URL 提取 "scheme://host" 部分（不含路径）。
// 例如："https://example.com/path/page" → "https://example.com"。
func baseFromURL(rawURL string) string {
	idx := strings.Index(rawURL, "://")
	if idx < 0 {
		return ""
	}
	rest := rawURL[idx+3:]
	slash := strings.Index(rest, "/")
	if slash < 0 {
		return rawURL // 无路径，直接返回整个 URL
	}
	return rawURL[:idx+3+slash]
}

// pathFromURL 从原始 URL 提取路径部分（不含域名）。
// 例如："https://example.com/path/page?q=1#top" → "/path/page"。
func pathFromURL(rawURL string) string {
	idx := strings.Index(rawURL, "://")
	if idx < 0 {
		return "/"
	}
	rest := rawURL[idx+3:]
	slash := strings.Index(rest, "/")
	if slash < 0 {
		return "/"
	}
	p := rest[slash:] // 从第一个斜杠开始即为路径
	// 去除查询字符串和锚点
	p = strings.SplitN(p, "?", 2)[0]
	p = strings.SplitN(p, "#", 2)[0]
	return p
}

// resolveURL 将相对 href 解析为绝对 URL，参考 base（协议+主机）和 basePath（当前页面路径）。
// 支持：http://、https:// 绝对 URL；// 协议相对 URL；/ 绝对路径；相对路径。
func resolveURL(base, basePath, href string) string {
	// 已经是绝对 URL，直接返回
	if strings.HasPrefix(href, "http://") || strings.HasPrefix(href, "https://") {
		return href
	}
	// 协议相对 URL（以 // 开头），补上协议
	if strings.HasPrefix(href, "//") {
		idx := strings.Index(base, "://")
		if idx < 0 {
			return ""
		}
		return base[:idx+1] + href
	}
	// 绝对路径（以 / 开头），拼接域名
	if strings.HasPrefix(href, "/") {
		return base + href
	}
	// 相对路径：基于当前页面目录拼接
	dir := path.Dir(basePath) // 提取当前页面的目录部分
	return base + path.Clean(dir+"/"+href) // path.Clean 规范化，去除多余的 ../ 等
}

// isSafeURL 检查 href 是否为安全的 HTTP(S) 链接。
// 过滤 javascript:、data:、mailto:、tel:、ftp: 等伪协议，
// 以及空 href 和仅包含锚点的 href。
func isSafeURL(href string) bool {
	if href == "" || href == "#" {
		return false
	}
	// 检查是否包含冒号（伪协议特征）
	colon := strings.Index(href, ":")
	if colon < 0 {
		return true // 无冒号：相对路径、绝对路径、协议相对路径，都是安全的
	}
	scheme := strings.ToLower(href[:colon])
	switch scheme {
	case "http", "https":
		return true
	default:
		// javascript:, data:, mailto:, tel:, ftp:, vbscript: 等全部拦截
		return false
	}
}

// NormalizeURL 将 URL 规范化为用于去重的标准形式。
// 1. 统一为小写 scheme 和 host
// 2. path.Clean 规范化路径（去除 ./、../）
// 3. 按 key 字典序排列 query 参数（消除 ?a=1&b=2 vs ?b=2&a=1 的差异）
// 4. 去除 fragment
// 5. 去除末尾斜杠（根路径 / 除外）
// 返回空字符串表示 URL 无效。
func NormalizeURL(rawURL string) string {
	u, err := url.Parse(rawURL)
	if err != nil {
		return ""
	}
	if u.Scheme != "http" && u.Scheme != "https" {
		return ""
	}
	if u.Host == "" {
		return ""
	}

	// 统一 scheme 和 host 为小写
	u.Scheme = strings.ToLower(u.Scheme)
	u.Host = strings.ToLower(u.Host)

	// 规范化路径
	if u.Path == "" {
		u.Path = "/"
	}
	u.Path = path.Clean(u.Path)

	// query 参数按 key 字典序排列
	if u.RawQuery != "" {
		u.RawQuery = sortQuery(u.RawQuery)
	}

	// 去除 fragment
	u.Fragment = ""

	// 去除末尾斜杠（根路径 / 除外）
	if len(u.Path) > 1 && strings.HasSuffix(u.Path, "/") {
		u.Path = strings.TrimRight(u.Path, "/")
	}

	return u.String()
}

// sortQuery 将 query 字符串的参数按 key 字典序排列，用于 URL 去重。
func sortQuery(query string) string {
	params, err := url.ParseQuery(query)
	if err != nil {
		return query
	}
	keys := make([]string, 0, len(params))
	for k := range params {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	// url.Values 编码后参数已排序且值已去重
	return params.Encode()
}
