// 只读诊断：检查 Geo 库与 page_views 地理字段，或对指定 IP 试查中文映射结果。
//
//	go run ./cmd/monitor-geo-check -db data/monitor.db
//	go run ./cmd/monitor-geo-check -db data/monitor.db -ip 14.109.35.246
package main

import (
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"git.iioio.com/freefire/jiang13-forum/service"
	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

func main() {
	dbPath := flag.String("db", filepath.Join("data", "monitor.db"), "含 page_views 的 SQLite（默认 data/monitor.db）")
	ipLookup := flag.String("ip", "", "对指定 IP 试查 BIN+ASN（不写库）")
	flag.Parse()

	abs, err := filepath.Abs(*dbPath)
	if err != nil {
		fatalf("解析路径失败: %v", err)
	}
	dataDir := filepath.Dir(abs)

	if *ipLookup == "" {
		if st, err := os.Stat(abs); err != nil || st.IsDir() {
			fatalf("数据库不存在: %s", abs)
		}
	}

	fmt.Printf("数据库: %s\n", abs)
	fmt.Printf("数据目录: %s\n\n", dataDir)

	suite := service.NewGeoIPService(dataDir)
	defer suite.Close()
	printGeoFiles(dataDir, suite)

	if ip := strings.TrimSpace(*ipLookup); ip != "" {
		lookupIP(suite, ip)
		fmt.Println()
		if _, err := os.Stat(abs); err != nil {
			return
		}
	}

	db, err := gorm.Open(sqlite.Open(abs), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		fatalf("打开数据库失败: %v", err)
	}
	sqlDB, err := db.DB()
	if err != nil {
		fatalf("获取连接失败: %v", err)
	}
	defer sqlDB.Close()
	sqlDB.SetMaxOpenConns(1)

	if !tableExists(db, "page_views") {
		fatalf("表 page_views 不存在（请确认指向 data/monitor.db，或尚未写入过浏览量）")
	}

	have := columnSet(db)
	printColumnPresence(have)

	since := time.Now().AddDate(0, 0, -30)
	var total, recentNonBot, withCountry, withRegion, withCity, withASN int64
	_ = db.Table("page_views").Count(&total).Error
	_ = db.Table("page_views").Where("created_at >= ? AND is_bot = ?", since, false).Count(&recentNonBot).Error

	fmt.Println("=== page_views 统计（近 30 日非 bot）===")
	fmt.Printf("总量:           %d\n", total)
	fmt.Printf("近 30 日非 bot: %d\n", recentNonBot)

	if have["country"] {
		_ = db.Table("page_views").Where("created_at >= ? AND is_bot = ? AND country <> ''", since, false).Count(&withCountry).Error
		fmt.Printf("有国家:         %d\n", withCountry)
	} else {
		fmt.Println("有国家:         (列缺失)")
	}
	if have["region"] || have["region_iso"] {
		cond := "created_at >= ? AND is_bot = ?"
		parts := []string{}
		if have["region"] {
			parts = append(parts, "region <> ''")
		}
		if have["region_iso"] {
			parts = append(parts, "region_iso <> ''")
		}
		_ = db.Table("page_views").
			Where(cond+" AND ("+strings.Join(parts, " OR ")+")", since, false).
			Count(&withRegion).Error
		fmt.Printf("有省字段:       %d\n", withRegion)
	} else {
		fmt.Println("有省字段:       (列缺失)")
	}
	if have["city"] {
		_ = db.Table("page_views").Where("created_at >= ? AND is_bot = ? AND city <> ''", since, false).Count(&withCity).Error
		fmt.Printf("有城市:         %d\n", withCity)
	} else {
		fmt.Println("有城市:         (列缺失)")
	}
	if have["asn"] {
		_ = db.Table("page_views").Where("created_at >= ? AND is_bot = ? AND asn > 0", since, false).Count(&withASN).Error
		fmt.Printf("有 ASN:         %d\n", withASN)
	} else {
		fmt.Println("有 ASN:         (列缺失)")
	}
	fmt.Println()

	fmt.Println("=== 近 30 日省级 Top（country / region / region_iso）===")
	if !(have["region"] || have["region_iso"]) {
		fmt.Println("(省列尚未迁移，跳过)")
	} else {
		type regionRow struct {
			Country   string
			Region    string
			RegionISO string `gorm:"column:region_iso"`
			Count     int64
		}
		parts := []string{}
		if have["region"] {
			parts = append(parts, "region <> ''")
		}
		if have["region_iso"] {
			parts = append(parts, "region_iso <> ''")
		}
		selParts := []string{}
		groupParts := []string{}
		if have["country"] {
			selParts = append(selParts, "country")
			groupParts = append(groupParts, "country")
		} else {
			selParts = append(selParts, "'' as country")
		}
		if have["region"] {
			selParts = append(selParts, "region")
			groupParts = append(groupParts, "region")
		} else {
			selParts = append(selParts, "'' as region")
		}
		if have["region_iso"] {
			selParts = append(selParts, "region_iso")
			groupParts = append(groupParts, "region_iso")
		} else {
			selParts = append(selParts, "'' as region_iso")
		}
		selParts = append(selParts, "COUNT(*) as count")
		var regions []regionRow
		_ = db.Table("page_views").
			Select(strings.Join(selParts, ", ")).
			Where("created_at >= ? AND is_bot = ? AND ("+strings.Join(parts, " OR ")+")", since, false).
			Group(strings.Join(groupParts, ", ")).
			Order("count DESC").
			Limit(20).
			Scan(&regions).Error
		if len(regions) == 0 {
			fmt.Println("(空)")
		} else {
			for i, r := range regions {
				fmt.Printf("%2d. %s | %s | %s | %d\n", i+1, r.Country, r.Region, r.RegionISO, r.Count)
			}
		}
	}
	fmt.Println()

	fmt.Println("=== 对账抽样（近 30 日非 bot）===")
	printGeoReconcileSamples(db, have, since)
	fmt.Println()

	fmt.Println("=== 样例行（近 30 日，最多 10 条）===")
	cols := []string{"id", "created_at", "path", "is_bot"}
	for _, c := range []string{"ip", "country", "region", "region_iso", "city", "asn", "as_org"} {
		if have[c] {
			cols = append(cols, c)
		}
	}
	type sample struct {
		ID        uint
		CreatedAt time.Time
		IP        string
		Country   string
		Region    string
		RegionISO string `gorm:"column:region_iso"`
		City      string
		ASN       uint
		ASOrg     string `gorm:"column:as_org"`
		Path      string
		IsBot     bool
	}
	var samples []sample
	_ = db.Table("page_views").
		Select(strings.Join(cols, ", ")).
		Where("created_at >= ?", since).
		Order("id DESC").
		Limit(10).
		Scan(&samples).Error
	if len(samples) == 0 {
		fmt.Println("(无近 30 日 pageview)")
	} else {
		for _, s := range samples {
			fmt.Printf("#%d %s ip=%s country=%q region=%q iso=%q city=%q asn=%d org=%q bot=%v path=%s\n",
				s.ID, s.CreatedAt.Format("2006-01-02 15:04:05"), s.IP,
				s.Country, s.Region, s.RegionISO, s.City, s.ASN, s.ASOrg, s.IsBot, s.Path)
		}
	}

	regionReady := have["region"] || have["region_iso"]
	if !regionReady || withRegion == 0 {
		fmt.Fprintln(os.Stderr, "")
		fmt.Fprintln(os.Stderr, "提示: 近 30 日无省级字段。中国地图「暂无省级访问数据」属预期。")
		if !regionReady {
			fmt.Fprintln(os.Stderr, "当前库缺 region/region_iso 列：请先启动一次主程序以完成 AutoMigrate。")
		}
		fmt.Fprintln(os.Stderr, "请确认数据目录已放置 IP2LOCATION-LITE-DB3.BIN 与 GeoLite2-ASN.mmdb，且有公网访问产生的 pageview。")
		fmt.Fprintln(os.Stderr, "本机/私网 IP 通常解不出省，属正常。")
		fmt.Fprintln(os.Stderr, "注意: 请求日志有地理 ≠ 概览地图/排行有数据（后者只读 page_views）。")
		os.Exit(2)
	}
}

func printGeoReconcileSamples(db *gorm.DB, have map[string]bool, since time.Time) {
	type sample struct {
		ID        uint
		CreatedAt time.Time
		IP        string
		Country   string
		Region    string
		RegionISO string `gorm:"column:region_iso"`
		City      string
		ASN       uint
		ASOrg     string `gorm:"column:as_org"`
		Path      string
	}
	selCols := []string{"id", "created_at", "path"}
	for _, c := range []string{"ip", "country", "region", "region_iso", "city", "asn", "as_org"} {
		if have[c] {
			selCols = append(selCols, c)
		}
	}
	sel := strings.Join(selCols, ", ")

	printBlock := func(title string, n int64, rows []sample) {
		fmt.Printf("%s: %d\n", title, n)
		if len(rows) == 0 {
			return
		}
		for _, s := range rows {
			fmt.Printf("  #%d %s ip=%s country=%q region=%q iso=%q city=%q asn=%d org=%q path=%s\n",
				s.ID, s.CreatedAt.Format("2006-01-02 15:04:05"), s.IP,
				s.Country, s.Region, s.RegionISO, s.City, s.ASN, s.ASOrg, s.Path)
		}
	}

	if have["city"] {
		var n int64
		_ = db.Table("page_views").
			Where("created_at >= ? AND is_bot = ? AND city <> '' AND city GLOB '[A-Za-z]*'", since, false).
			Count(&n).Error
		var rows []sample
		_ = db.Table("page_views").
			Select(sel).
			Where("created_at >= ? AND is_bot = ? AND city <> '' AND city GLOB '[A-Za-z]*'", since, false).
			Order("id DESC").
			Limit(5).
			Scan(&rows).Error
		printBlock("英文城市名（可能未中文化）", n, rows)
	} else {
		fmt.Println("英文城市名: (city 列缺失)")
	}

	if have["region"] || have["region_iso"] {
		cond := "created_at >= ? AND is_bot = ? AND country <> ''"
		emptyParts := []string{}
		if have["region"] {
			emptyParts = append(emptyParts, "(region = '' OR region IS NULL OR region = '-')")
		}
		if have["region_iso"] {
			emptyParts = append(emptyParts, "(region_iso = '' OR region_iso IS NULL)")
		}
		where := cond + " AND (" + strings.Join(emptyParts, " AND ") + ")"
		var n int64
		_ = db.Table("page_views").Where(where, since, false).Count(&n).Error
		var rows []sample
		_ = db.Table("page_views").
			Select(sel).
			Where(where, since, false).
			Order("id DESC").
			Limit(5).
			Scan(&rows).Error
		printBlock("有国家但省为空", n, rows)
	} else {
		fmt.Println("有国家但省为空: (省列缺失)")
	}

	if have["asn"] {
		var n int64
		_ = db.Table("page_views").
			Where("created_at >= ? AND is_bot = ? AND (asn = 0 OR asn IS NULL)", since, false).
			Count(&n).Error
		var rows []sample
		_ = db.Table("page_views").
			Select(sel).
			Where("created_at >= ? AND is_bot = ? AND (asn = 0 OR asn IS NULL)", since, false).
			Order("id DESC").
			Limit(5).
			Scan(&rows).Error
		printBlock("asn=0（无运营商）", n, rows)
	} else {
		fmt.Println("asn=0: (asn 列缺失)")
	}
}

func printGeoFiles(dataDir string, suite *service.GeoIPService) {
	fmt.Println("=== Geo 数据文件 ===")
	v4, v6, asn, country := suite.Paths()
	for _, item := range []struct {
		name string
		path string
		ok   bool
	}{
		{"IP2LOCATION-LITE-DB3.BIN", v4, suite.BINV4Available()},
		{"IP2LOCATION-LITE-DB3.IPV6.BIN", v6, suite.BINV6Available()},
		{"GeoLite2-ASN.mmdb", asn, suite.ASNAvailable()},
		{"GeoLite2-Country.mmdb", country, suite.CountryAvailable()},
	} {
		st, err := os.Stat(item.path)
		if err != nil {
			fmt.Printf("%s: 不存在\n", item.name)
			continue
		}
		loaded := "未加载"
		if item.ok {
			loaded = "已加载"
		}
		fmt.Printf("%s: 存在 (%d bytes, mtime %s) · %s\n", item.name, st.Size(), st.ModTime().Format(time.RFC3339), loaded)
	}
	fmt.Println()
}

func lookupIP(suite *service.GeoIPService, ipStr string) {
	fmt.Printf("=== IP 试查 %s（中文映射后）===\n", ipStr)
	info := suite.Lookup(ipStr)
	if info.Country == "" && info.Region == "" && info.City == "" && info.ASN == 0 {
		fmt.Println("未解析到地理信息（请确认 BIN/ASN 文件存在且 IP 为公网地址）")
		return
	}
	fmt.Printf("国家 ISO: %s (%s)\n", info.Country, service.CountryLabelZh(info.Country))
	if info.Region != "" || info.RegionISO != "" {
		fmt.Printf("省/州: %s / %s\n", info.Region, info.RegionISO)
	}
	if info.City != "" {
		fmt.Printf("城市: %s\n", info.City)
	}
	if info.ASN > 0 || info.ASOrg != "" {
		fmt.Printf("ASN: AS%d %s\n", info.ASN, info.ASOrg)
	}
}

func tableExists(db *gorm.DB, name string) bool {
	var n int
	err := db.Raw(
		`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?`, name,
	).Scan(&n).Error
	return err == nil && n > 0
}

func columnSet(db *gorm.DB) map[string]bool {
	type colRow struct {
		Name string `gorm:"column:name"`
	}
	var cols []colRow
	_ = db.Raw(`PRAGMA table_info(page_views)`).Scan(&cols).Error
	have := map[string]bool{}
	for _, c := range cols {
		have[c.Name] = true
	}
	return have
}

func printColumnPresence(have map[string]bool) {
	need := []string{"ip", "country", "region", "region_iso", "city", "asn", "as_org"}
	fmt.Println("=== page_views 列 ===")
	missing := false
	for _, n := range need {
		if have[n] {
			fmt.Printf("%s: 有\n", n)
		} else {
			fmt.Printf("%s: 缺失\n", n)
			missing = true
		}
	}
	fmt.Println()
	if missing {
		fmt.Fprintln(os.Stderr, "提示: 缺列时请先启动一次主程序以完成监控库 AutoMigrate。")
		fmt.Fprintln(os.Stderr, "")
	}
}

func fatalf(format string, args ...any) {
	fmt.Fprintf(os.Stderr, format+"\n", args...)
	os.Exit(1)
}
