// Package crawler implements the HTTP fetching layer with robots.txt compliance,
// per-host rate limiting, redirect tracking, and encoding detection.
// crawler 包负责 HTTP 请求层：遵守 robots.txt、主机限流、追踪重定向、自动检测字符集。
package crawler

import (
	"fmt"       // 字符串格式化（构建 robots.txt URL、错误信息）
	"io"        // IO 接口（读取响应体）
	"net"       // IP 地址解析（SSRF 防护）
	"net/http"  // HTTP 客户端
	"net/url"   // URL 解析
	"strings"   // 字符串操作
	"sync"      // 互斥锁（保护限流表和 robots.txt 缓存）
	"time"      // 时间（限流间隔计算、robots.txt 缓存过期）

	"golang.org/x/net/html/charset"                  // HTML 字符集自动检测（将各种编码转为 UTF-8）
	"golang.org/x/text/encoding/simplifiedchinese"  // GBK → UTF-8 转换兜底
)

// ErrCrawl 表示爬取过程中的预期错误（404、被 robots.txt 禁止、非 HTML 类型等）。
// 此类错误由 FetchSafe 静默丢弃（返回 nil, nil）。
type ErrCrawl struct {
	Msg string // 错误描述文本
}

// Error 实现 error 接口，返回错误描述。
func (e *ErrCrawl) Error() string { return e.Msg }

// FetchResult 封装一次成功抓取的完整结果。
type FetchResult struct {
	Body        string            // 解码后的 HTML 正文（UTF-8）
	FinalURL    string            // 经过所有重定向后的最终 URL
	Redirects   map[string]string // 永久重定向（301/308）映射：原始 URL → 最终 URL
	ServerType  string            // HTTP Server 响应头（如 "nginx/1.18"）
}

// Fetcher 是一个可复用的 HTTP 客户端，内置 robots.txt 合规检查和按主机限流。
type Fetcher struct {
	client    *http.Client // HTTP 客户端（包含重定向和超时控制）
	userAgent string       // HTTP 请求的 User-Agent 头
	cooldown  time.Duration // 同一主机相邻两次请求的最小间隔

	rateMu   sync.Mutex          // 保护 lastHit 限流表的互斥锁
	lastHit  map[string]time.Time // 主机名 → 上次请求时间（用于计算限流等待）

	robotsMu sync.Mutex                      // 保护 robots 缓存的互斥锁
	robots   map[string]*robotsEntry // 主机名 → 该主机的 robots.txt 解析结果（含缓存时间）
}

// robotsEntry 缓存单台主机的 robots.txt 解析结果。
type robotsEntry struct {
	rules     []robotsRule // 解析后的规则列表
	fetchedAt time.Time    // 缓存时间（用于判断是否过期，24h 后重新抓取）
}

// robotsRule 一条 robots.txt 规则，对应一个 User-Agent 块。
type robotsRule struct {
	userAgent string   // 适用的爬虫名称（"*" 表示全部）
	disallow  []string // Disallow 路径列表
	allow     []string // Allow 路径列表（优先于 disallow）
}

// NewFetcher 创建一个新的 Fetcher 实例。
// userAgent：发出的 HTTP 请求的 User-Agent；cooldown：同一主机相邻请求的最小间隔。
func NewFetcher(userAgent string, cooldown time.Duration) *Fetcher {
	return &Fetcher{
		client: &http.Client{
			Timeout: 30 * time.Second, // 默认单次请求超时 30 秒
			CheckRedirect: func(req *http.Request, via []*http.Request) error {
				// 跟随重定向最多 10 次，防止重定向循环
				if len(via) >= 10 {
					return fmt.Errorf("too many redirects")
				}
				return nil
			},
		},
		userAgent: userAgent,
		cooldown:  cooldown,
		lastHit:   make(map[string]time.Time), // 限流表初始化
		robots:    make(map[string]*robotsEntry), // robots.txt 缓存初始化
	}
}

// Fetch 抓取指定 URL，遵守 robots.txt 和主机限流。
// polite=false 时跳过 robots.txt 检查和限流（用于搜索服务在线抓摘要）。
func (f *Fetcher) Fetch(rawURL string, polite bool, timeout time.Duration, sizeLimit int) (*FetchResult, error) {
	return f.fetchWithHistory(rawURL, polite, timeout, sizeLimit)
}

// FetchSafe 封装 Fetch，在遇到预期爬取错误（404/disallowed/非 HTML）时返回 (nil, nil)。
// 调用方无需区分错误类型，直接跳过即可。
func (f *Fetcher) FetchSafe(rawURL string, polite bool, timeout time.Duration, sizeLimit int) (*FetchResult, error) {
	res, err := f.fetchWithHistory(rawURL, polite, timeout, sizeLimit)
	if _, ok := err.(*ErrCrawl); ok {
		return nil, nil // 预期错误，静默丢弃
	}
	return res, err
}

// fetchWithHistory 执行实际 HTTP 请求，追踪永久重定向。
func (f *Fetcher) fetchWithHistory(rawURL string, polite bool, timeout time.Duration, sizeLimit int) (*FetchResult, error) {
	// 解析 URL 提取主机名
	parsed, err := url.Parse(rawURL)
	if err != nil {
		return nil, &ErrCrawl{Msg: "invalid url: " + err.Error()}
	}
	host := parsed.Host

	// polite 模式：先限流，再检查 robots.txt
	if polite {
		f.rateLimit(host)
		if !f.robotsAllowed(rawURL, host) {
			return nil, &ErrCrawl{Msg: "disallowed by robots.txt"}
		}
	}

	// 追踪永久重定向（301/308）
	redirects := make(map[string]string)
	client := &http.Client{
		Timeout: timeout,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			if len(via) >= 10 {
				return fmt.Errorf("too many redirects")
			}
			// SSRF 防护：拒绝重定向到内网 IP 或非标端口
			if err := isSafeRedirect(req.URL); err != nil {
				return err
			}
			// 记录永久重定向
			if req.Response != nil && (req.Response.StatusCode == 301 || req.Response.StatusCode == 308) {
				from := via[len(via)-1].URL.String()
				to := req.URL.String()
				redirects[from] = to
			}
			return nil
		},
	}

	// 对初始 URL 也做 SSRF 检查
	if err := isSafeRedirect(parsed); err != nil {
		return nil, &ErrCrawl{Msg: err.Error()}
	}

	// 构造 GET 请求
	req, _ := http.NewRequest("GET", rawURL, nil)
	req.Header.Set("User-Agent", f.userAgent)

	// 发送请求
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close() // 读取完毕后关闭响应体

	// 检查 HTTP 状态码
	if resp.StatusCode == 404 {
		return nil, &ErrCrawl{Msg: "404 not found"}
	}
	if resp.StatusCode >= 400 {
		return nil, &ErrCrawl{Msg: fmt.Sprintf("HTTP %d", resp.StatusCode)}
	}

	// 检查 Content-Type，必须是 HTML 才继续
	ct := resp.Header.Get("Content-Type")
	if !strings.Contains(ct, "text/html") {
		return nil, &ErrCrawl{Msg: "not html: " + ct}
	}

	// 解码响应体（自动检测字符集转为 UTF-8）
	body, err := decodeBody(resp.Body, ct, sizeLimit)
	if err != nil {
		return nil, err
	}

	return &FetchResult{
		Body:       body,
		FinalURL:   resp.Request.URL.String(), // 重定向后的最终 URL
		Redirects:   redirects,
		ServerType: resp.Header.Get("Server"),
	}, nil
}

// rateLimit 检查并强制执行主机限流：若距上次请求不足 cooldown 秒则 sleep 等待。
func (f *Fetcher) rateLimit(host string) {
	f.rateMu.Lock()
	last, ok := f.lastHit[host]
	now := time.Now()
	f.lastHit[host] = now

	// 限流表超过 10000 条时清理两倍 cooldown 时间之前的过期项，防止内存泄漏
	if len(f.lastHit) > 10000 {
		cutoff := now.Add(-f.cooldown * 2)
		for k, v := range f.lastHit {
			if v.Before(cutoff) {
				delete(f.lastHit, k)
			}
		}
	}
	f.rateMu.Unlock()

	// 计算需要等待的时间
	if ok {
		elapsed := now.Sub(last)
		if elapsed < f.cooldown {
			time.Sleep(f.cooldown - elapsed)
		}
	}
}

// robotsAllowed 根据 robots.txt 规则判断某 URL 是否允许爬取。
func (f *Fetcher) robotsAllowed(rawURL, host string) bool {
	// 尝试从缓存读取（加锁保护）
	f.robotsMu.Lock()
	entry, ok := f.robots[host]
	f.robotsMu.Unlock()

	// 缓存不存在或已过期（超过 24 小时）则重新抓取并解析
	if !ok || time.Since(entry.fetchedAt) > 24*time.Hour {
		entry = f.fetchRobots(host, rawURL)
		f.robotsMu.Lock()
		f.robots[host] = entry
		f.robotsMu.Unlock()
	}

	// 解析 URL 路径
	parsed, err := url.Parse(rawURL)
	if err != nil {
		return false
	}
	path := parsed.Path
	if path == "" {
		path = "/"
	}

	// 遍历所有规则，找到适用的 User-Agent
	for _, rule := range entry.rules {
		if rule.userAgent != "*" && !strings.EqualFold(rule.userAgent, f.userAgent) {
			continue
		}
		// Allow 优先检查（更高优先级）
		for _, a := range rule.allow {
			if strings.HasPrefix(path, a) {
				return true
			}
		}
		// 再检查 Disallow
		for _, dis := range rule.disallow {
			if dis != "" && strings.HasPrefix(path, dis) {
				return false
			}
		}
	}
	return true // 默认允许
}

// fetchRobots 抓取并解析某主机的 robots.txt 文件。
func (f *Fetcher) fetchRobots(host, exampleURL string) *robotsEntry {
	entry := &robotsEntry{fetchedAt: time.Now()} // 初始化空条目（抓取失败时默认允许全部）
	scheme := "https"
	if strings.HasPrefix(exampleURL, "http://") {
		scheme = "http"
	}
	robotsURL := fmt.Sprintf("%s://%s/robots.txt", scheme, host)

	// robots.txt 单独请求，超时 5 秒
	client := &http.Client{Timeout: 5 * time.Second}
	req, _ := http.NewRequest("GET", robotsURL, nil)
	req.Header.Set("User-Agent", f.userAgent)
	resp, err := client.Do(req)
	if err != nil || resp.StatusCode != 200 {
		return entry // robots.txt 不可用时默认允许爬取
	}
	defer resp.Body.Close()

	// 最多读取 256KB（大部分 robots.txt 远小于此大小）
	body, err := io.ReadAll(io.LimitReader(resp.Body, 256*1024))
	if err != nil {
		return entry
	}
	entry.rules = parseRobots(string(body))
	return entry
}

// parseRobots 最小化 robots.txt 解析器。
// 支持 User-agent、Disallow、Allow 三种指令，忽略注释和空行。
func parseRobots(content string) []robotsRule {
	var rules []robotsRule
	var current *robotsRule
	for _, line := range strings.Split(content, "\n") {
		line = strings.TrimSpace(line)
		// 去除行内注释
		if idx := strings.Index(line, "#"); idx >= 0 {
			line = line[:idx]
		}
		if line == "" {
			// 空行结束当前块
			if current != nil {
				rules = append(rules, *current)
				current = nil
			}
			continue
		}
		parts := strings.SplitN(line, ":", 2)
		if len(parts) != 2 {
			continue
		}
		key := strings.TrimSpace(strings.ToLower(parts[0]))
		val := strings.TrimSpace(parts[1])
		switch key {
		case "user-agent":
			// 新建一个 User-Agent 块
			if current == nil {
				current = &robotsRule{userAgent: val}
			} else {
				current.userAgent = val
			}
		case "disallow":
			if current != nil {
				current.disallow = append(current.disallow, val)
			}
		case "allow":
			if current != nil {
				current.allow = append(current.allow, val)
			}
		}
	}
	// 最后一个块
	if current != nil {
		rules = append(rules, *current)
	}
	return rules
}

// decodeBody 从响应体读取最多 sizeLimit 字节，自动检测字符集并转为 UTF-8 字符串。
// sizeLimit <= 0 时不限制大小。
func decodeBody(r io.Reader, contentType string, sizeLimit int) (string, error) {
	var reader io.Reader = r
	if sizeLimit > 0 {
		reader = io.LimitReader(r, int64(sizeLimit)) // 限制读取字节数，防止大文件撑爆内存
	}

	// 使用 golang.org/x/net/html/charset 自动检测 HTML 编码并转为 UTF-8
	utf8Reader, err := charset.NewReader(reader, contentType)
	if err != nil {
		// charset 检测失败时，先读取原始字节，再尝试 GBK 兜底
		data, readErr := io.ReadAll(reader)
		if readErr != nil {
			return "", readErr
		}
		// 将 GBK 字节流转为 UTF-8 字符串
		utf8Bytes, convErr := simplifiedchinese.GBK.NewDecoder().Bytes(data)
		if convErr == nil {
			return string(utf8Bytes), nil
		}
		// 转换失败则返回原始字节（可能乱码但不崩溃）
		return string(data), nil
	}
	data, err := io.ReadAll(utf8Reader)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

// isPrivateIP 检查 IP 是否为私有/回环/链路本地地址。
func isPrivateIP(ip net.IP) bool {
	privateRanges := []struct {
		network *net.IPNet
	}{
		// 10.0.0.0/8    — RFC 1918 私有网络
		{mustParseCIDR("10.0.0.0/8")},
		// 172.16.0.0/12 — RFC 1918 私有网络
		{mustParseCIDR("172.16.0.0/12")},
		// 192.168.0.0/16 — RFC 1918 私有网络
		{mustParseCIDR("192.168.0.0/16")},
		// 127.0.0.0/8   — 回环地址
		{mustParseCIDR("127.0.0.0/8")},
		// 169.254.0.0/16 — 链路本地（AWS/GCP 元数据服务）
		{mustParseCIDR("169.254.0.0/16")},
		// ::1/128       — IPv6 回环
		{mustParseCIDR("::1/128")},
		// fe80::/10     — IPv6 链路本地
		{mustParseCIDR("fe80::/10")},
		// fc00::/7      — IPv6 唯一本地地址
		{mustParseCIDR("fc00::/7")},
	}
	for _, r := range privateRanges {
		if r.network.Contains(ip) {
			return true
		}
	}
	return false
}

// mustParseCIDR 解析 CIDR，失败时 panic（仅用于编译期常量）。
func mustParseCIDR(s string) *net.IPNet {
	_, network, err := net.ParseCIDR(s)
	if err != nil {
		panic("invalid CIDR: " + s)
	}
	return network
}

// isSafeRedirect 检查重定向目标是否安全（非内网 IP）。
// 用于防止 SSRF 攻击：恶意服务器将爬虫重定向到内网服务。
func isSafeRedirect(u *url.URL) error {
	host := u.Hostname()
	// 解析 IP 地址
	ip := net.ParseIP(host)
	if ip == nil {
		// 域名（非 IP），允许（DNS 解析由系统处理，端口不限）
		return nil
	}
	// IP 直连：检查是否为私有地址
	if isPrivateIP(ip) {
		return fmt.Errorf("blocked: private IP %s", ip)
	}
	return nil
}
