package monitor

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"nano-router/config"
	"nano-router/mihomo"
)

const (
	checkInterval    = 30 * time.Second
	speedInterval    = 5 * time.Second
	ipCheckInterval  = 10 * time.Minute
	minuteSlots      = 60
)

type MinuteSlot struct {
	Minute  int64            `json:"minute"`
	Status string           `json:"status"`
	Pings  map[string]int   `json:"pings,omitempty"`
	PingCF int              `json:"ping_cf,omitempty"`
	PingGG int              `json:"ping_gg,omitempty"`
}

type ConnectivityState struct {
	DirectUp       bool          `json:"direct_up"`
	VPNUp          bool          `json:"vpn_up"`
	DirectUpSince  string        `json:"direct_up_since,omitempty"`
	VPNUpSince     string        `json:"vpn_up_since,omitempty"`
	MinutesD       []MinuteSlot  `json:"minutes_direct"`
	MinutesV       []MinuteSlot  `json:"minutes_vpn"`
	EndpointNames  []string      `json:"endpoint_names"`
}

type Sample struct {
	Time time.Time `json:"time"`
	RxPS float64   `json:"rx_bps"`
	TxPS float64   `json:"tx_bps"`
}

type IPInfo struct {
	IP      string `json:"ip"`
	Country string `json:"country"`
	CC      string `json:"cc"`
}

type SystemStats struct {
	CPUPct   float64 `json:"cpu_pct"`
	MemPct   float64 `json:"mem_pct"`
	MemTotal uint64  `json:"mem_total"`
	MemUsed  uint64  `json:"mem_used"`
	MemFree  uint64  `json:"mem_free"`
	Uptime   int64   `json:"uptime"`
}

type DashboardData struct {
	System       SystemStats      `json:"system"`
	Mihomo       MihomoStatus     `json:"mihomo"`
	Connectivity ConnectivityState `json:"connectivity"`
	GatewayIface string           `json:"gateway_iface"`
	TrafficAvg   []Sample         `json:"traffic_avg"`
	TrafficReal  []Sample         `json:"traffic_real"`
	IPDirect     *IPInfo          `json:"ip_direct"`
	IPVPN        *IPInfo          `json:"ip_vpn"`
}

type MihomoStatus struct {
	Running bool `json:"running"`
	PID     int  `json:"pid,omitempty"`
}

var (
	mu            sync.RWMutex
	connState     ConnectivityState
	minuteSlotsD  [minuteSlots]MinuteSlot
	minuteSlotsV  [minuteSlots]MinuteSlot
	gatewayIface  string
	prevRx       uint64
	prevTx       uint64
	prevSpeedTime time.Time
	curRxPS      float64
	curTxPS      float64
	avgSamples   []Sample
	realSamples  []Sample
	ipDirect     *IPInfo
	ipVPN        *IPInfo
	ipDirectT    time.Time
	ipVPNT       time.Time

	directUpSince  *time.Time
	vpnUpSince     *time.Time
	wasDirectUp    bool
	wasVPNUp       bool

	prevCPUTotal  uint64
	prevCPUIdle   uint64
	cpuInitialized bool

	stopCh chan struct{}
)

func Start() {
	stopCh = make(chan struct{})
	gatewayIface = detectGatewayIface()
	for i := 0; i < minuteSlots; i++ {
		minuteSlotsD[i] = MinuteSlot{Status: "no_data"}
		minuteSlotsV[i] = MinuteSlot{Status: "no_data"}
	}
	go connectivityLoop()
	go speedLoop()
	go ipCheckLoop()
}

func Stop() {
	close(stopCh)
}

func GetData() DashboardData {
	mu.RLock()
	defer mu.RUnlock()

	cfg, _ := config.Load()
	var epNames []string
	if cfg != nil && len(cfg.Connectivity.Direct) > 0 {
		for _, ep := range cfg.Connectivity.Direct {
			epNames = append(epNames, ep.Name)
		}
	} else {
		epNames = []string{"Cloudflare", "Google"}
	}
	connState.EndpointNames = epNames

	d := DashboardData{
		System:       readSystemStats(),
		Mihomo:       getMihomoRunning(),
		Connectivity: connState,
		GatewayIface: gatewayIface,
		IPDirect:     ipDirect,
		IPVPN:        ipVPN,
	}

	d.Connectivity.MinutesD = buildMinuteSlots(minuteSlotsD[:])
	d.Connectivity.MinutesV = buildMinuteSlots(minuteSlotsV[:])

	now := time.Now()
	cutoff10m := now.Add(-10 * time.Minute)
	cutoff1m := now.Add(-1 * time.Minute)

	filteredAvg := make([]Sample, 0, len(avgSamples))
	for _, s := range avgSamples {
		if !s.Time.Before(cutoff10m) {
			filteredAvg = append(filteredAvg, s)
		}
	}
	d.TrafficAvg = filteredAvg

	filteredReal := make([]Sample, 0, len(realSamples))
	for _, s := range realSamples {
		if !s.Time.Before(cutoff1m) {
			filteredReal = append(filteredReal, s)
		}
	}
	d.TrafficReal = filteredReal

	if d.IPDirect != nil && time.Since(ipDirectT) > ipCheckInterval+time.Minute {
		d.IPDirect = nil
	}
	if d.IPVPN != nil && time.Since(ipVPNT) > ipCheckInterval+time.Minute {
		d.IPVPN = nil
	}

	return d
}

func getMihomoRunning() MihomoStatus {
	st := mihomo.Status()
	r, _ := st["running"].(bool)
	s := MihomoStatus{Running: r}
	if r {
		if pid, ok := st["pid"].(int); ok {
			s.PID = pid
		}
	}
	return s
}

func readSystemStats() SystemStats {
	s := SystemStats{}
	readCPUStats(&s)
	readMemStats(&s)
	readUptime(&s)
	return s
}

func readCPUStats(s *SystemStats) {
	f, err := os.Open("/proc/stat")
	if err != nil {
		return
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	if !scanner.Scan() {
		return
	}
	line := scanner.Text()
	if !strings.HasPrefix(line, "cpu ") {
		return
	}
	fields := strings.Fields(line)
	if len(fields) < 5 {
		return
	}

	var total, idle uint64
	for i := 1; i < len(fields) && i <= 8; i++ {
		v, _ := strconv.ParseUint(fields[i], 10, 64)
		total += v
		if i == 4 {
			idle = v
		}
	}

	if cpuInitialized && prevCPUTotal > 0 {
		dTotal := total - prevCPUTotal
		dIdle := idle - prevCPUIdle
		if dTotal > 0 {
			s.CPUPct = float64(dTotal-dIdle) / float64(dTotal) * 100.0
		}
	}
	prevCPUTotal = total
	prevCPUIdle = idle
	cpuInitialized = true
}

func readMemStats(s *SystemStats) {
	f, err := os.Open("/proc/meminfo")
	if err != nil {
		return
	}
	defer f.Close()

	var memTotal, memFree, memAvailable, buffers, cached uint64
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		parts := strings.Fields(line)
		if len(parts) < 2 {
			continue
		}
		val, _ := strconv.ParseUint(parts[1], 10, 64)
		switch {
		case strings.HasPrefix(parts[0], "MemTotal:"):
			memTotal = val
		case strings.HasPrefix(parts[0], "MemFree:"):
			memFree = val
		case strings.HasPrefix(parts[0], "MemAvailable:"):
			memAvailable = val
		case strings.HasPrefix(parts[0], "Buffers:"):
			buffers = val
		case strings.HasPrefix(parts[0], "Cached:"):
			cached = val
		}
	}

	s.MemTotal = memTotal * 1024
	s.MemFree = memFree * 1024
	used := memTotal - memAvailable
	if memAvailable == 0 {
		used = memTotal - memFree - buffers - cached
	}
	s.MemUsed = used * 1024
	if memTotal > 0 {
		s.MemPct = float64(used) / float64(memTotal) * 100.0
	}
}

func readUptime(s *SystemStats) {
	f, err := os.Open("/proc/uptime")
	if err != nil {
		return
	}
	defer f.Close()
	var upSec float64
	fmt.Fscanf(f, "%f", &upSec)
	s.Uptime = int64(upSec)
}

func detectGatewayIface() string {
	out, err := exec.Command("ip", "route", "show", "default").Output()
	if err != nil {
		return ""
	}
	for _, line := range strings.Split(string(out), "\n") {
		fields := strings.Fields(line)
		for i, f := range fields {
			if f == "dev" && i+1 < len(fields) {
				return fields[i+1]
			}
		}
	}
	return ""
}

func readIfaceBytes(iface string) (rx, tx uint64) {
	f, err := os.Open("/proc/net/dev")
	if err != nil {
		return 0, 0
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		colon := strings.Index(line, ":")
		if colon < 0 {
			continue
		}
		name := strings.TrimSpace(line[:colon])
		if name != iface {
			continue
		}
		fields := strings.Fields(line[colon+1:])
		if len(fields) >= 10 {
			rx, _ = strconv.ParseUint(fields[0], 10, 64)
			tx, _ = strconv.ParseUint(fields[8], 10, 64)
		}
		return
	}
	return 0, 0
}

func connectivityLoop() {
	ticker := time.NewTicker(checkInterval)
	defer ticker.Stop()

	checkConnectivity()

	for {
		select {
		case <-ticker.C:
			checkConnectivity()
		case <-stopCh:
			return
		}
	}
}

func checkConnectivity() {
	directResult := checkDirectConnectivity()
	vpnResult := checkVPNConnectivity()

	now := time.Now()
	minuteIdx := int(now.Unix()/60) % minuteSlots

	mu.Lock()
	defer mu.Unlock()

	connState.DirectUp = directResult.Up
	connState.VPNUp = vpnResult.Up

	if directResult.Up && !wasDirectUp {
		directUpSince = &time.Time{}
		*directUpSince = now
	} else if !directResult.Up {
		directUpSince = nil
	}
	wasDirectUp = directResult.Up

	mihomoRunning := mihomo.IsRunning()
	if vpnResult.Up && !wasVPNUp {
		vpnUpSince = &time.Time{}
		*vpnUpSince = now
	} else if !vpnResult.Up {
		vpnUpSince = nil
	}
	wasVPNUp = vpnResult.Up

	if directUpSince != nil {
		dur := now.Sub(*directUpSince)
		connState.DirectUpSince = fmtDuration(dur)
	} else {
		connState.DirectUpSince = ""
	}
	if vpnUpSince != nil {
		dur := now.Sub(*vpnUpSince)
		connState.VPNUpSince = fmtDuration(dur)
	} else {
		connState.VPNUpSince = ""
	}

	statusD := "up"
	if !directResult.Up {
		statusD = "down"
	}
	statusV := "up"
	if !vpnResult.Up {
		statusV = "down"
	}
	if !mihomoRunning {
		statusV = "no_data"
	}

	dSlot := MinuteSlot{Status: statusD, Pings: directResult.Pings}
	if v, ok := directResult.Pings["Cloudflare"]; ok {
		dSlot.PingCF = v
	}
	if v, ok := directResult.Pings["Google"]; ok {
		dSlot.PingGG = v
	}
	minuteSlotsD[minuteIdx] = mergeSlot(minuteSlotsD[minuteIdx], dSlot)

	vSlot := MinuteSlot{Status: statusV, Pings: make(map[string]int)}
	if vpnResult.Up {
		vSlot.Pings = vpnResult.Pings
		if v, ok := vpnResult.Pings["Cloudflare"]; ok {
			vSlot.PingCF = v
		}
		if v, ok := vpnResult.Pings["Google"]; ok {
			vSlot.PingGG = v
		}
	}
	minuteSlotsV[minuteIdx] = mergeSlot(minuteSlotsV[minuteIdx], vSlot)
}

func fmtDuration(d time.Duration) string {
	s := int(d.Seconds())
	if s < 0 {
		s = 0
	}
	days := s / 86400
	s %= 86400
	hours := s / 3600
	s %= 3600
	mins := s / 60
	secs := s % 60

	parts := []string{}
	if days > 0 {
		parts = append(parts, fmt.Sprintf("%dd", days))
	}
	if hours > 0 || days > 0 {
		parts = append(parts, fmt.Sprintf("%dh", hours))
	}
	if mins > 0 || hours > 0 || days > 0 {
		parts = append(parts, fmt.Sprintf("%dm", mins))
	}
	parts = append(parts, fmt.Sprintf("%ds", secs))
	return strings.Join(parts, " ")
}

func mergeSlot(existing, new MinuteSlot) MinuteSlot {
	merged := MinuteSlot{
		Status: new.Status,
		Pings:  make(map[string]int),
	}
	if new.Status == "down" && existing.Status == "up" {
		merged.Status = "down"
	} else if new.Status == "up" && existing.Status == "down" {
		merged.Status = "down"
	}
	for k, v := range existing.Pings {
		merged.Pings[k] = v
	}
	for k, v := range new.Pings {
		if v > 0 {
			merged.Pings[k] = v
		}
	}
	if v, ok := merged.Pings["Cloudflare"]; ok {
		merged.PingCF = v
	}
	if v, ok := merged.Pings["Google"]; ok {
		merged.PingGG = v
	}
	return merged
}

type connResult struct {
	Up     bool
	Pings  map[string]int
	PingCF int
	PingGG int
}

func checkDirectConnectivity() connResult {
	result := connResult{Pings: make(map[string]int)}
	client := &http.Client{Timeout: 8 * time.Second}

	cfg, _ := config.Load()
	if cfg == nil || len(cfg.Connectivity.Direct) == 0 {
		def := config.DefaultConnectivity()
		cfg = &config.AppConfig{Connectivity: def}
	}
	endpoints := cfg.Connectivity.Direct

	anyUp := false
	for _, ep := range endpoints {
		start := time.Now()
		ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
		req, err := http.NewRequestWithContext(ctx, "GET", ep.URL, nil)
		if err != nil {
			cancel()
			continue
		}
		resp, err := client.Do(req)
		elapsed := time.Since(start)
		cancel()
		if err != nil {
			continue
		}
		resp.Body.Close()
		ms := int(elapsed.Milliseconds())
		if resp.StatusCode >= 200 && resp.StatusCode < 400 {
			anyUp = true
			result.Pings[ep.Name] = ms
		}
	}
	result.Up = anyUp
	return result
}

func checkVPNConnectivity() connResult {
	result := connResult{Pings: make(map[string]int)}
	if !mihomo.IsRunning() {
		return result
	}

	mixedPort := getMihomoMixedPort()
	if mixedPort == 0 {
		mixedPort = 7890
	}

	proxyAddr := fmt.Sprintf("127.0.0.1:%d", mixedPort)
	proxyURL, _ := url.Parse("http://" + proxyAddr)

	proxyClient := &http.Client{
		Timeout: 10 * time.Second,
		Transport: &http.Transport{
			Proxy: http.ProxyURL(proxyURL),
		},
	}

	cfg, _ := config.Load()
	if cfg == nil || len(cfg.Connectivity.ViaProxy) == 0 {
		def := config.DefaultConnectivity()
		cfg = &config.AppConfig{Connectivity: def}
	}
	endpoints := cfg.Connectivity.ViaProxy

	anyUp := false
	for _, ep := range endpoints {
		start := time.Now()
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		req, err := http.NewRequestWithContext(ctx, "GET", ep.URL, nil)
		if err != nil {
			cancel()
			continue
		}
		resp, err := proxyClient.Do(req)
		elapsed := time.Since(start)
		cancel()
		if err != nil {
			continue
		}
		resp.Body.Close()
		ms := int(elapsed.Milliseconds())
		if resp.StatusCode >= 200 && resp.StatusCode < 400 {
			anyUp = true
			result.Pings[ep.Name] = ms
		}
	}
	result.Up = anyUp
	return result
}

func getMihomoMixedPort() int {
	cfg, err := mihomo.LoadConfig()
	if err != nil {
		return 7890
	}
	if mp, ok := cfg["mixed-port"]; ok {
		switch v := mp.(type) {
		case int:
			return v
		case float64:
			return int(v)
		}
	}
	return 7890
}

func speedLoop() {
	iface := gatewayIface
	if iface == "" {
		iface = detectGatewayIface()
		gatewayIface = iface
	}

	if iface == "" {
		return
	}

	rx, tx := readIfaceBytes(iface)
	prevRx = rx
	prevTx = tx
	prevSpeedTime = time.Now()

	ticker := time.NewTicker(speedInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			captureSpeedSample()
		case <-stopCh:
			return
		}
	}
}

func captureSpeedSample() {
	iface := gatewayIface
	if iface == "" {
		iface = detectGatewayIface()
		if iface == "" {
			return
		}
		mu.Lock()
		gatewayIface = iface
		mu.Unlock()
	}

	rx, tx := readIfaceBytes(iface)
	now := time.Now()

	mu.Lock()
	defer mu.Unlock()

	dt := now.Sub(prevSpeedTime).Seconds()
	if dt > 0 && prevSpeedTime.Before(now) {
		curRxPS = float64(rx-prevRx) * 8.0 / dt
		curTxPS = float64(tx-prevTx) * 8.0 / dt
	}

	prevRx = rx
	prevTx = tx
	prevSpeedTime = now

	sample := Sample{Time: now, RxPS: curRxPS, TxPS: curTxPS}
	realSamples = append(realSamples, sample)

	cutoff1m := now.Add(-1 * time.Minute)
	filtered := make([]Sample, 0, len(realSamples))
	for _, s := range realSamples {
		if !s.Time.Before(cutoff1m) {
			filtered = append(filtered, s)
		}
	}
	realSamples = filtered

	avgSamples = append(avgSamples, sample)
	cutoff10m := now.Add(-10 * time.Minute)
	filteredAvg := make([]Sample, 0, len(avgSamples))
	for _, s := range avgSamples {
		if !s.Time.Before(cutoff10m) {
			filteredAvg = append(filteredAvg, s)
		}
	}
	avgSamples = filteredAvg
}

func ipCheckLoop() {
	checkIPs()
	ticker := time.NewTicker(ipCheckInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			checkIPs()
		case <-stopCh:
			return
		}
	}
}

func checkIPs() {
	directInfo := fetchIPDirect()
	if directInfo != nil {
		mu.Lock()
		ipDirect = directInfo
		ipDirectT = time.Now()
		mu.Unlock()
	}

	if mihomo.IsRunning() {
		vpnInfo := fetchIPVPN()
		if vpnInfo != nil {
			mu.Lock()
			ipVPN = vpnInfo
			ipVPNT = time.Now()
			mu.Unlock()
		}
	}
}

func fetchIPDirect() *IPInfo {
	client := &http.Client{Timeout: 10 * time.Second}
	return fetchIPFromServices(client)
}

func fetchIPVPN() *IPInfo {
	mixedPort := getMihomoMixedPort()
	proxyAddr := fmt.Sprintf("127.0.0.1:%d", mixedPort)
	proxyURL, _ := url.Parse("http://" + proxyAddr)

	client := &http.Client{
		Timeout: 10 * time.Second,
		Transport: &http.Transport{
			Proxy: http.ProxyURL(proxyURL),
		},
	}
	return fetchIPFromServices(client)
}

func fetchIPFromServices(client *http.Client) *IPInfo {
	services := []struct {
		url   string
		parse func(body []byte) *IPInfo
	}{
		{
			"https://ipinfo.io/json",
			func(body []byte) *IPInfo {
				var data struct {
					IP      string `json:"ip"`
					Country string `json:"country"`
				}
				if err := json.Unmarshal(body, &data); err != nil || data.IP == "" {
					return nil
				}
				return &IPInfo{IP: data.IP, Country: data.Country, CC: strings.ToLower(data.Country)}
			},
		},
		{
			"https://ipapi.co/json/",
			func(body []byte) *IPInfo {
				var data struct {
					IP          string `json:"ip"`
					CountryCode string `json:"country_code"`
					CountryName string `json:"country_name"`
				}
				if err := json.Unmarshal(body, &data); err != nil || data.IP == "" {
					return nil
				}
				cc := strings.ToLower(data.CountryCode)
				country := data.CountryName
				if country == "" {
					country = data.CountryCode
				}
				return &IPInfo{IP: data.IP, Country: country, CC: cc}
			},
		},
	}

	for _, svc := range services {
		resp, err := client.Get(svc.url)
		if err != nil {
			continue
		}
		body, err := io.ReadAll(io.LimitReader(resp.Body, 4096))
		resp.Body.Close()
		if err != nil {
			continue
		}
		info := svc.parse(body)
		if info != nil {
			return info
		}
	}

	return nil
}

func buildMinuteSlots(slots []MinuteSlot) []MinuteSlot {
	result := make([]MinuteSlot, minuteSlots)
	now := time.Now()
	currentMinute := now.Unix() / 60

	for i := 0; i < minuteSlots; i++ {
		minuteOffset := minuteSlots - 1 - i
		minuteIdx := int((currentMinute - int64(minuteOffset)) % minuteSlots)
		if minuteIdx < 0 {
			minuteIdx += minuteSlots
		}
		result[i] = slots[minuteIdx]
		result[i].Minute = currentMinute - int64(minuteOffset)
		if result[i].Status == "" {
			result[i].Status = "no_data"
		}
	}

	return result
}

var countryFlagRe = regexp.MustCompile(`^[a-zA-Z]{2}$`)

func FlagEmoji(cc string) string {
	if !countryFlagRe.MatchString(cc) {
		return ""
	}
	cc = strings.ToUpper(cc)
	runes := []rune(cc)
	if len(runes) != 2 {
		return ""
	}
	base := rune(0x1F1E6)
	return string(base+(runes[0]-'A')) + string(base+(runes[1]-'A'))
}