package collector

import (
	"bufio"
	"context"
	"crypto/tls"
	"database/sql"
	"fmt"
	"io"
	"net"
	"net/http"
	"strings"
	"time"

	"host-agent/internal/config"
	"host-agent/internal/payload"

	_ "github.com/go-sql-driver/mysql"
	_ "github.com/lib/pq"
)

// CollectService는 단일 서비스 인스턴스의 앱 지표를 수집하고,
// runtime(native/docker)에 따라 리소스 오버레이(컨테이너/프로세스 지표)를 덧붙입니다.
func CollectService(cfg config.ServiceConfig) payload.ServicePayload {
	var status string
	var metrics map[string]interface{}

	switch cfg.Type {
	case "postgres":
		status, metrics = collectPostgres(cfg)
	case "mysql":
		status, metrics = collectMySQL(cfg)
	case "redis":
		status, metrics = collectRedis(cfg)
	case "nginx":
		status, metrics = collectNginx(cfg)
	default: // fastapi/django/nodejs/nextjs/nuxtjs 등 HTTP 앱
		status, metrics = collectHTTP(cfg)
	}
	if metrics == nil {
		metrics = map[string]interface{}{}
	}

	image := ""
	// 실행 방식별 리소스 오버레이
	if cfg.Runtime == "docker" && cfg.Container != "" {
		if cm, cstatus, cimage := dockerStats(cfg.Container); cm != nil {
			metrics["container"] = cm
			image = cimage
			// 컨테이너가 정상 구동 중이 아니면 서비스 상태에 반영
			if cstatus != "" && cstatus != "running" {
				status = cstatus
			}
		}
	} else if cfg.Runtime == "native" {
		if pm := nativeProcStats(cfg); pm != nil {
			metrics["process"] = pm
		}
	}

	return payload.ServicePayload{
		Type:      cfg.Type,
		Name:      cfg.Name,
		Runtime:   cfg.Runtime,
		Container: cfg.Container,
		Image:     image,
		Status:    status,
		Metrics:   metrics,
	}
}

// ---- PostgreSQL ----
func collectPostgres(cfg config.ServiceConfig) (string, map[string]interface{}) {
	dsn := cfg.PostgresDSN()
	if dsn == "" {
		return "down", map[string]interface{}{"error": "db(권장) 또는 dsn 미설정"}
	}
	db, err := sql.Open("postgres", dsn)
	if err != nil {
		return "down", map[string]interface{}{"error": err.Error()}
	}
	defer db.Close()
	db.SetConnMaxLifetime(5 * time.Second)
	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()
	if err := db.PingContext(ctx); err != nil {
		return "down", map[string]interface{}{"error": err.Error()}
	}

	m := map[string]interface{}{}
	var total, active int
	if err := db.QueryRowContext(ctx,
		"SELECT count(*), count(*) FILTER (WHERE state = 'active') FROM pg_stat_activity").Scan(&total, &active); err == nil {
		m["total_connections"], m["active_connections"] = total, active
	}
	var size int64
	if err := db.QueryRowContext(ctx, "SELECT pg_database_size(current_database())").Scan(&size); err == nil {
		m["db_size_bytes"] = size
	}
	var locks int
	if err := db.QueryRowContext(ctx, "SELECT count(*) FROM pg_locks").Scan(&locks); err == nil {
		m["locks_count"] = locks
	}
	var cacheHit float64
	if err := db.QueryRowContext(ctx, `SELECT COALESCE(round(sum(heap_blks_hit)*100.0/(sum(heap_blks_read)+sum(heap_blks_hit)+0.0001),2),100.0) FROM pg_statio_user_tables`).Scan(&cacheHit); err == nil {
		m["cache_hit_ratio"] = cacheHit
	}
	var idxHit float64
	if err := db.QueryRowContext(ctx, `SELECT COALESCE(round(sum(idx_blks_hit)*100.0/(sum(idx_blks_read)+sum(idx_blks_hit)+0.0001),2),100.0) FROM pg_statio_user_indexes`).Scan(&idxHit); err == nil {
		m["index_hit_ratio"] = idxHit
	}
	var deadlocks, tempBytes int64
	if err := db.QueryRowContext(ctx, "SELECT COALESCE(sum(deadlocks),0), COALESCE(sum(temp_bytes),0) FROM pg_stat_database WHERE datname = current_database()").Scan(&deadlocks, &tempBytes); err == nil {
		m["deadlocks"], m["temp_bytes"] = deadlocks, tempBytes
	}
	var dead, live int64
	if err := db.QueryRowContext(ctx, "SELECT COALESCE(sum(n_dead_tup),0), COALESCE(sum(n_live_tup),0) FROM pg_stat_user_tables").Scan(&dead, &live); err == nil {
		m["dead_tuples"], m["live_tuples"] = dead, live
	}
	return "up", m
}

// ---- MySQL ----
func collectMySQL(cfg config.ServiceConfig) (string, map[string]interface{}) {
	dsn := cfg.MySQLDSN()
	if dsn == "" {
		return "down", map[string]interface{}{"error": "db(권장) 또는 dsn 미설정"}
	}
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		return "down", map[string]interface{}{"error": err.Error()}
	}
	defer db.Close()
	db.SetConnMaxLifetime(5 * time.Second)
	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()
	if err := db.PingContext(ctx); err != nil {
		return "down", map[string]interface{}{"error": err.Error()}
	}

	stat := map[string]string{}
	if rows, err := db.QueryContext(ctx, "SHOW GLOBAL STATUS"); err == nil {
		for rows.Next() {
			var k, v string
			if rows.Scan(&k, &v) == nil {
				stat[k] = v
			}
		}
		rows.Close()
	}
	var vn, maxConn string
	_ = db.QueryRowContext(ctx, "SHOW VARIABLES LIKE 'max_connections'").Scan(&vn, &maxConn)

	m := map[string]interface{}{
		"threads_connected": atoiSafe(stat["Threads_connected"]),
		"threads_running":   atoiSafe(stat["Threads_running"]),
		"max_connections":   atoiSafe(maxConn),
		"questions":         atoiSafe(stat["Questions"]),
		"slow_queries":      atoiSafe(stat["Slow_queries"]),
		"aborted_connects":  atoiSafe(stat["Aborted_connects"]),
		"uptime":            atoiSafe(stat["Uptime"]),
	}
	return "up", m
}

// redisHostPort는 redis 주소를 TCP 연결용 host:port로 정규화합니다.
//
//	허용 입력: "host:port", "host:port/0"(DB 번호), "redis://[user:pass@]host:port/0"
//	INFO 명령은 DB 선택과 무관하므로 host:port만 추출합니다.
func redisHostPort(addr string) string {
	if addr == "" {
		return "localhost:6379"
	}
	addr = strings.TrimPrefix(addr, "redis://")
	addr = strings.TrimPrefix(addr, "rediss://")
	if at := strings.LastIndex(addr, "@"); at >= 0 { // 자격증명(user:pass@) 제거
		addr = addr[at+1:]
	}
	if slash := strings.Index(addr, "/"); slash >= 0 { // 뒤의 /db 제거
		addr = addr[:slash]
	}
	if addr == "" {
		return "localhost:6379"
	}
	return addr
}

// ---- Redis (경량 RESP INFO 클라이언트) ----
func collectRedis(cfg config.ServiceConfig) (string, map[string]interface{}) {
	addr := redisHostPort(cfg.Addr)
	conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
	if err != nil {
		return "down", map[string]interface{}{"error": err.Error()}
	}
	defer conn.Close()
	_ = conn.SetDeadline(time.Now().Add(3 * time.Second))
	r := bufio.NewReader(conn)

	if cfg.Password != "" {
		fmt.Fprintf(conn, "AUTH %s\r\n", cfg.Password)
		_, _ = r.ReadString('\n') // +OK / -ERR 무시 (best-effort)
	}
	fmt.Fprint(conn, "INFO\r\n")
	line, err := r.ReadString('\n')
	if err != nil || len(line) == 0 || line[0] != '$' {
		return "down", map[string]interface{}{"error": "INFO 응답 파싱 실패"}
	}
	n := int(atoiSafe(strings.TrimSpace(line[1:])))
	if n <= 0 {
		return "down", map[string]interface{}{}
	}
	buf := make([]byte, n)
	if _, err := io.ReadFull(r, buf); err != nil {
		return "down", map[string]interface{}{"error": err.Error()}
	}
	info := parseKVLines(string(buf), ":")

	hits, misses := atofSafe(info["keyspace_hits"]), atofSafe(info["keyspace_misses"])
	hitRate := 100.0
	if hits+misses > 0 {
		hitRate = hits / (hits + misses) * 100
	}
	m := map[string]interface{}{
		"used_memory":       atoiSafe(info["used_memory"]),
		"maxmemory":         atoiSafe(info["maxmemory"]),
		"connected_clients": atoiSafe(info["connected_clients"]),
		"keyspace_hits":     int64(hits),
		"keyspace_misses":   int64(misses),
		"hit_rate":          roundF(hitRate, 1),
		"evicted_keys":      atoiSafe(info["evicted_keys"]),
		"ops_per_sec":       atoiSafe(info["instantaneous_ops_per_sec"]),
		"uptime":            atoiSafe(info["uptime_in_seconds"]),
		"mem_frag_ratio":    atofSafe(info["mem_fragmentation_ratio"]),
	}
	return "up", m
}

// ---- nginx (stub_status) ----
func collectNginx(cfg config.ServiceConfig) (string, map[string]interface{}) {
	target := cfg.StatusURL
	if target == "" {
		target = cfg.URL
	}
	if target == "" {
		return "down", map[string]interface{}{"error": "status_url 미설정"}
	}
	start := time.Now()
	resp, err := httpClient.Get(target)
	if err != nil {
		return "down", map[string]interface{}{"error": err.Error()}
	}
	defer resp.Body.Close()
	latency := time.Since(start).Milliseconds()
	body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
	m := parseNginxStatus(string(body))
	m["latency_ms"] = latency
	m["status_code"] = resp.StatusCode
	addCertExpiry(m, resp)
	if resp.StatusCode != http.StatusOK {
		return "degraded", m
	}
	return "up", m
}

// parseNginxStatus는 stub_status 출력에서 지표를 추출합니다.
//
//	Active connections: 5
//	server accepts handled requests
//	 1000 1000 1234
//	Reading: 0 Writing: 1 Waiting: 4
func parseNginxStatus(s string) map[string]interface{} {
	m := map[string]interface{}{}
	lines := strings.Split(s, "\n")
	for i, ln := range lines {
		f := strings.Fields(ln)
		switch {
		case strings.HasPrefix(ln, "Active connections:") && len(f) >= 3:
			m["active"] = atoiSafe(f[2])
		case strings.HasPrefix(ln, "Reading:") && len(f) >= 6:
			m["reading"], m["writing"], m["waiting"] = atoiSafe(f[1]), atoiSafe(f[3]), atoiSafe(f[5])
		case strings.HasPrefix(strings.TrimSpace(ln), "server accepts"):
			if i+1 < len(lines) {
				nums := strings.Fields(lines[i+1])
				if len(nums) >= 3 {
					m["accepts"], m["handled"], m["requests"] = atoiSafe(nums[0]), atoiSafe(nums[1]), atoiSafe(nums[2])
				}
			}
		}
	}
	return m
}

// certDaysLeft는 TLS 연결의 리프 인증서 만료까지 남은 일수를 반환합니다.
// HTTP(비TLS) 연결이거나 인증서가 없으면 ok=false. 이미 만료된 경우 음수.
func certDaysLeft(state *tls.ConnectionState, now time.Time) (int, bool) {
	if state == nil || len(state.PeerCertificates) == 0 {
		return 0, false
	}
	return int(state.PeerCertificates[0].NotAfter.Sub(now).Hours() / 24), true
}

// addCertExpiry는 HTTPS 응답이면 metrics에 cert_expiry_days를 덧붙입니다.
func addCertExpiry(m map[string]interface{}, resp *http.Response) {
	if d, ok := certDaysLeft(resp.TLS, time.Now()); ok {
		m["cert_expiry_days"] = d
	}
}

// ---- HTTP 앱 프로브 (fastapi/django/nodejs/nextjs/nuxtjs ...) ----
func collectHTTP(cfg config.ServiceConfig) (string, map[string]interface{}) {
	if cfg.URL == "" {
		return "down", map[string]interface{}{"error": "url 미설정", "ok": false}
	}
	start := time.Now()
	resp, err := httpClient.Get(cfg.URL)
	if err != nil {
		return "down", map[string]interface{}{"error": err.Error(), "ok": false}
	}
	defer resp.Body.Close()
	_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 8192))
	latency := time.Since(start).Milliseconds()
	m := map[string]interface{}{
		"latency_ms":  latency,
		"status_code": resp.StatusCode,
		"ok":          resp.StatusCode >= 200 && resp.StatusCode < 400,
	}
	addCertExpiry(m, resp)
	status := "up"
	if resp.StatusCode >= 500 {
		status = "degraded"
	} else if resp.StatusCode >= 400 {
		status = "degraded"
	}
	return status, m
}

// parseKVLines는 "key<sep>value" 형식의 멀티라인 텍스트를 맵으로 파싱합니다(redis INFO 등).
func parseKVLines(s, sep string) map[string]string {
	out := map[string]string{}
	for _, ln := range strings.Split(s, "\n") {
		ln = strings.TrimRight(ln, "\r")
		if ln == "" || strings.HasPrefix(ln, "#") {
			continue
		}
		if idx := strings.Index(ln, sep); idx > 0 {
			out[ln[:idx]] = ln[idx+len(sep):]
		}
	}
	return out
}
