// sese-engine — Go rewrite
// Go 版 sese-engine：个人搜索引擎的主入口文件。
//
// 所有模块（爬虫、收获服务器、搜索服务器、反向链接计算）均作为 goroutine 在同一进程中启动。
// 主线程阻塞等待系统信号（Ctrl-C / SIGTERM），收到后优雅退出。
//
// 运行方式：
//
//	cd golang && go run . [--storage ./savedata] [--entry https://zh.wikipedia.org/]
package main

import (
	"context"
	"flag"          // 命令行参数解析
	"fmt"           // 格式化（搜索服务端口）
	"log"           // 日志输出
	"os"            // 操作系统信号
	"os/signal"     // 信号捕获
	"path/filepath" // 路径处理
	"syscall"       // 系统调用（SIGTERM）
	"time"          // 时间（刷盘间隔）

	"sese-engine/analyzer" // 文本分析和关键词提取
	"sese-engine/backlink" // 反向链接（繁荣值）计算
	"sese-engine/config"   // 全局配置
	"sese-engine/crawler"  // BFS 爬虫
	"sese-engine/info"     // info 服务（繁荣表、调整表、屏蔽词）
	"sese-engine/mysql"    // MySQL 数据库连接
	"sese-engine/redis"    // Redis 连接
	"sese-engine/search"   // 搜索服务器（内嵌收获服务）
	"sese-engine/storage"  // 持久化存储
)

// initConfig 检查并初始化配置文件
// 如果 savedata/config.yml 不存在，则从模板生成
func initConfig() error {
	configDir := "savedata"
	configPath := filepath.Join(configDir, "config.yml")

	// 检查配置文件是否已存在
	if _, err := os.Stat(configPath); err == nil {
		// 配置文件已存在，直接返回
		return nil
	}

	// 确保 data 目录存在
	if err := os.MkdirAll(configDir, 0755); err != nil {
		return fmt.Errorf("failed to create config directory: %v", err)
	}

	// 从模板生成配置文件
	defaultConfig := `# SESE Engine 配置文件
# 程序实际加载的配置文件

# 索引 / 存储相关限制
index:
  max_urls_per_key: 11000          # 每个索引词最多保存的 URL 数量上限
  max_same_domain_per_key: 20      # 同一域名在每个索引词下最多出现的次数
  big_clean_threshold: 2000000     # 内存中累计多少条索引后触发一次刷盘清理
  max_new_urls_per_key: 10000      # 每次刷盘时，每个索引词最多写入的新 URL 数量上限
  min_urls_for_new_key: 3          # 新索引词如果 URL 数少于该值则丢弃，不写入磁盘

# 爬虫行为相关配置
crawler:
  spider_name: "Haibara_AI_spider"       # HTTP 请求的 User-Agent 标识
  cooldown: 3                      # 同一主机相邻两次请求的最小间隔（秒），用于遵守 robots.txt 和避免被封
  workers: 22                      # 爬虫并发 goroutine 数量
  crawl_focus: 0.7                 # 域名集中度因子，越大越倾向在少量域名内深挖，越小越分散
  max_keywords_per_page: 250       # 单个页面最多提取的关键词数量
  max_epoch: 100                   # BFS 爬取的最大轮次上限
  expected_prosper_ratio: 0.6      # 队列中预期"繁荣"域名（高反向链接）的占比，用于调度决策
  entry_url: "https://haibara.ai/"  # BFS 爬取的起始入口 URL
  max_page_size: 5242880           # 单个页面最大抓取字节数（0=不限，默认 5MB）
  recrawl_max_age: 2592000         # URL 过期时间（秒），超过此时间的 URL 允许被重爬，默认 30 天
  recrawl_check_interval: 3600     # 运行期间检查过期 URL 的间隔（秒），默认 1 小时
  recrawl_batch_size: 500          # 每次检查最多释放多少个过期 URL

# 搜索结果排序权重配置
search:
  use_online_snippet: true         # 是否在线抓取摘要（搜索时实时抓取页面补充摘要）
  online_snippet_timeout: 3        # 在线抓取摘要的超时时间（秒）
  weight_daily_decay: 0.996        # 页面年龄的时间衰减因子（每天乘以此系数）
  language_weight: 0.5             # 语种匹配权重：与查询语种一致时加分
  consecutive_key_weight: 1.3      # 连续关键词命中权重：多词连续出现时加分
  backlink_weight: 1.0             # 反向链接权重：指向该 URL 的链接越多得分越高
  server_port: 50082                  # 搜索服务和收获服务的统一 HTTP 监听端口
  flush_interval_seconds: 300       # 定期刷盘间隔（秒）：将内存索引批量写入磁盘

# 反向链接（PageRank 类）计算相关配置
backlink:
  baseline: 200000                 # 反向链接得分归一化的除数（用于将原始链接数映射到 [0,1] 区间）

# 存储根目录路径，相对于进程启动时的工作目录
storage:
  path: "./savedata"

# 各模块 Prometheus 监控指标的 HTTP 端口
prometheus:
  crawler_port: 14950              # 爬虫模块的 metrics 端口
  backlink_port: 14952             # 反向链接计算模块的 metrics 端口
  search_port: 14953               # 搜索服务（含收获功能）模块的 metrics 端口
`

	if err := os.WriteFile(configPath, []byte(defaultConfig), 0644); err != nil {
		return fmt.Errorf("failed to write config file: %v", err)
	}

	log.Printf("Generated default config file: %s", configPath)
	return nil
}

// loadConfig 从 savedata/config.yml 加载配置
func loadConfig() error {
	if err := config.LoadFromSavedata(); err != nil {
		return fmt.Errorf("failed to load config: %v", err)
	}
	log.Printf("Config loaded successfully from savedata/config.yml")
	return nil
}

func main() {
	// ---- 0. 初始化配置文件 ----
	if err := initConfig(); err != nil {
		log.Fatalf("failed to init config: %v", err)
	}

	// ---- 0.5 加载配置文件 ----
	if err := loadConfig(); err != nil {
		log.Fatalf("failed to load config: %v", err)
	}

	// ---- 命令行参数 ----
	// --storage：存储根目录路径，默认使用 config.StoragePath
	storageDir := flag.String("storage", config.StoragePath, "path to savedata directory")
	// --entry：BFS 爬取的起始 URL，默认使用 config.EntryURL()（维基百科中文首页）
	entryURL := flag.String("entry", config.EntryURL(), "BFS crawl entry URL")
	// --stopwords：屏蔽词 JSON 文件路径
	stopWords := flag.String("stopwords", "/savedata/标点符号.json", "path to stop-words JSON")
	flag.Parse()

	// 设置日志格式：时间戳 + 短文件名
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	log.Printf("sese-engine starting  storage=%s  entry=%s", *storageDir, *entryURL)

	// ---- 1. Redis 连接（高性能内存存储）----
	if err := redis.Open(); err != nil {
		log.Fatalf("failed to open redis: %v", err)
	}
	defer redis.Close()

	// ---- 1.1 MySQL 连接（持久化存储，默认关闭）----
	var flusher *mysql.Flusher
	if config.MySQLEnabled() {
		if err := mysql.Open(); err != nil {
			log.Fatalf("failed to open mysql: %v", err)
		}
		defer mysql.Close()

		// 从 MySQL 恢复数据到 Redis（如 Redis 数据丢失）
		// 仅当 Redis 为空时才执行恢复，避免覆盖已有数据
		ctx := context.Background()
		size, _ := redis.Client.DBSize(ctx).Result()
		if size > 0 {
			log.Printf("[mysql-restore] Redis has %d keys, skipping restore", size)
		} else if err := mysql.RestoreFromMySQLToRedis(redis.Client); err != nil {
			log.Printf("[mysql-restore] warning: %v", err)
		}

		// MySQL 刷盘器：定期将 Redis 数据刷到 MySQL
		flusher = mysql.NewFlusher(redis.Client, 5*time.Minute, 1000)
		flusher.Start()
		defer flusher.Stop()
	} else {
		log.Println("[mysql] disabled in config, skipping MySQL init")
	}

	// ---- 1.3 存储层：初始化 Redis 存储 ----
	store := storage.NewRedisStoreV2()
	if err := store.Init(); err != nil {
		log.Fatalf("failed to init redis store: %v", err)
	}
	defer store.Close()

	// ---- 2. Info 服务：加载繁荣表、调整表和屏蔽词 ----
	infoSvc := info.New(*storageDir)

	// ---- 3. Analyzer：初始化分词器和语言检测器 ----
	// modelPath 参数已废弃（lingua-go 使用内置模型，无需外部文件）
	anal, err := analyzer.New("", *stopWords)
	if err != nil {
		log.Fatalf("failed to init analyzer: %v", err)
	}
	defer anal.Close()

	// ---- 4. 搜索服务器（默认 :80）：对外提供搜索 API，同时内嵌收获服务（统一端口）
	searchSrv := search.New(store, infoSvc, anal)
	go func() {
		addr := fmt.Sprintf(":%d", config.SearchServerPort())
		if err := searchSrv.ListenAndServe(addr, config.UnixSocket()); err != nil {
			log.Fatalf("[search] fatal: %v", err)
		}
	}()

	// ---- 6. 反向链接计算器：每 48 小时运行一次 ----
	bl := backlink.New(store, *storageDir)
	searchSrv.SetBacklinkRunner(bl)
	go bl.Run()

	// ---- 7. 爬虫：从入口 URL 开始 BFS 爬取 ----
	// 从 info 服务获取繁荣表快照，用于调度优先级决策
	prosperMap := infoSvc.ProsperMap()
	crawl := crawler.New(store, anal, prosperMap)
	searchSrv.SetCrawler(crawl) // 注入爬虫用于 Priority URL 立即触发
	go crawl.Run(*entryURL, config.MaxEpoch())

	log.Println("all modules started — press Ctrl-C to stop")

	// ---- 优雅退出 ----
	// 阻塞等待 SIGINT（Ctrl-C）或 SIGTERM 信号
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
	<-quit
	log.Println("shutdown signal received, initiating graceful shutdown...")

	// 通知爬虫停止（不阻塞，等待爬虫内部协调）
	crawl.Stop()

	// 等待爬虫完全停止（包括 priority worker）
	crawl.WaitUntilStopped()
	log.Println("crawler stopped")

	// 最后刷盘（Redis → Disk，条件刷 MySQL），确保数据不丢失
	log.Println("flushing index to disk...")
	searchSrv.Flush()
	if flusher != nil {
		log.Println("flushing data to mysql...")
		flusher.RunAll() // 同步执行一次 MySQL 刷盘
	}
	log.Println("shutdown complete")
}
