package report

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"time"
)

const rqliteBase = "http://localhost:5001"

// collectRQLite queries the local RQLite HTTP API to build a health report.
func collectRQLite() *RQLiteReport {
	r := &RQLiteReport{}

	// 1. GET /status — core Raft and node metadata.
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	statusBody, err := httpGet(ctx, rqliteBase+"/status")
	if err != nil {
		r.Responsive = false
		return r
	}

	var status map[string]interface{}
	if err := json.Unmarshal(statusBody, &status); err != nil {
		r.Responsive = false
		return r
	}
	r.Responsive = true

	// Extract fields from the nested status JSON.
	r.RaftState = getNestedString(status, "store", "raft", "state")
	r.LeaderAddr = getNestedString(status, "store", "leader", "addr")
	r.LeaderID = getNestedString(status, "store", "leader", "node_id")
	r.NodeID = getNestedString(status, "store", "node_id")
	r.Term = uint64(getNestedFloat(status, "store", "raft", "current_term"))
	r.Applied = uint64(getNestedFloat(status, "store", "raft", "applied_index"))
	r.Commit = uint64(getNestedFloat(status, "store", "raft", "commit_index"))
	r.FsmPending = uint64(getNestedFloat(status, "store", "raft", "fsm_pending"))
	r.LastContact = getNestedString(status, "store", "raft", "last_contact")
	r.Voter = getNestedBool(status, "store", "raft", "voter")
	r.DBSize = getNestedString(status, "store", "sqlite3", "db_size_friendly")
	r.Uptime = getNestedString(status, "http", "uptime")
	r.Version = getNestedString(status, "build", "version")
	r.Goroutines = int(getNestedFloat(status, "runtime", "num_goroutine"))

	// HeapMB: bytes → MB.
	heapBytes := getNestedFloat(status, "runtime", "memory", "heap_alloc")
	if heapBytes > 0 {
		r.HeapMB = int(heapBytes / (1024 * 1024))
	}

	// NumPeers may be a number or a string in the JSON; handle both.
	r.NumPeers = getNestedInt(status, "store", "raft", "num_peers")

	// 2. GET /nodes?nonvoters — cluster node list.
	{
		ctx2, cancel2 := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel2()

		if body, err := httpGet(ctx2, rqliteBase+"/nodes?nonvoters"); err == nil {
			var rawNodes map[string]struct {
				Addr      string  `json:"addr"`
				Reachable bool    `json:"reachable"`
				Leader    bool    `json:"leader"`
				Voter     bool    `json:"voter"`
				Time      float64 `json:"time"`
				Error     string  `json:"error"`
			}
			if err := json.Unmarshal(body, &rawNodes); err == nil {
				r.Nodes = make(map[string]RQLiteNodeInfo, len(rawNodes))
				for id, n := range rawNodes {
					r.Nodes[id] = RQLiteNodeInfo{
						Reachable: n.Reachable,
						Leader:    n.Leader,
						Voter:     n.Voter,
						TimeMS:    n.Time * 1000, // seconds → milliseconds
						Error:     n.Error,
					}
				}
			}
		}
	}

	// 3. GET /readyz — readiness probe.
	{
		ctx3, cancel3 := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel3()

		req, err := http.NewRequestWithContext(ctx3, http.MethodGet, rqliteBase+"/readyz", nil)
		if err == nil {
			if resp, err := http.DefaultClient.Do(req); err == nil {
				resp.Body.Close()
				r.Ready = resp.StatusCode == http.StatusOK
			}
		}
	}

	// 4. POST /db/query?level=strong — strong read test.
	{
		ctx4, cancel4 := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel4()

		payload := []byte(`["SELECT 1"]`)
		req, err := http.NewRequestWithContext(ctx4, http.MethodPost, rqliteBase+"/db/query?level=strong", bytes.NewReader(payload))
		if err == nil {
			req.Header.Set("Content-Type", "application/json")
			if resp, err := http.DefaultClient.Do(req); err == nil {
				io.Copy(io.Discard, resp.Body)
				resp.Body.Close()
				r.StrongRead = resp.StatusCode == http.StatusOK
			}
		}
	}

	// 5. GET /debug/vars — error counters.
	{
		ctx5, cancel5 := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel5()

		if body, err := httpGet(ctx5, rqliteBase+"/debug/vars"); err == nil {
			var vars map[string]interface{}
			if err := json.Unmarshal(body, &vars); err == nil {
				r.DebugVars = &RQLiteDebugVarsReport{
					QueryErrors:      jsonUint64(vars, "api_query_errors"),
					ExecuteErrors:    jsonUint64(vars, "api_execute_errors"),
					RemoteExecErrors: jsonUint64(vars, "api_remote_exec_errors"),
					LeaderNotFound:   jsonUint64(vars, "store_leader_not_found"),
					SnapshotErrors:   jsonUint64(vars, "snapshot_errors"),
					ClientRetries:    jsonUint64(vars, "client_retries"),
					ClientTimeouts:   jsonUint64(vars, "client_timeouts"),
				}
			}
		}
	}

	return r
}

// ---------------------------------------------------------------------------
// Nested-map extraction helpers
// ---------------------------------------------------------------------------

// getNestedString traverses nested map[string]interface{} values and returns
// the final value as a string. Returns "" if any key is missing or the leaf
// is not a string.
func getNestedString(m map[string]interface{}, keys ...string) string {
	v := getNestedValue(m, keys...)
	if v == nil {
		return ""
	}
	if s, ok := v.(string); ok {
		return s
	}
	return fmt.Sprintf("%v", v)
}

// getNestedFloat traverses nested maps and returns the leaf as a float64.
// JSON numbers are decoded as float64 by encoding/json into interface{}.
func getNestedFloat(m map[string]interface{}, keys ...string) float64 {
	v := getNestedValue(m, keys...)
	if v == nil {
		return 0
	}
	switch n := v.(type) {
	case float64:
		return n
	case json.Number:
		if f, err := n.Float64(); err == nil {
			return f
		}
	case string:
		if f, err := strconv.ParseFloat(n, 64); err == nil {
			return f
		}
	}
	return 0
}

// getNestedBool traverses nested maps and returns the leaf as a bool.
func getNestedBool(m map[string]interface{}, keys ...string) bool {
	v := getNestedValue(m, keys...)
	if v == nil {
		return false
	}
	if b, ok := v.(bool); ok {
		return b
	}
	return false
}

// getNestedInt traverses nested maps and returns the leaf as an int.
// Handles both numeric and string representations (RQLite sometimes
// returns num_peers as a string).
func getNestedInt(m map[string]interface{}, keys ...string) int {
	v := getNestedValue(m, keys...)
	if v == nil {
		return 0
	}
	switch n := v.(type) {
	case float64:
		return int(n)
	case json.Number:
		if i, err := n.Int64(); err == nil {
			return int(i)
		}
	case string:
		if i, err := strconv.Atoi(n); err == nil {
			return i
		}
	}
	return 0
}

// getNestedValue walks through nested map[string]interface{} following the
// given key path and returns the leaf value, or nil if any step fails.
func getNestedValue(m map[string]interface{}, keys ...string) interface{} {
	if len(keys) == 0 {
		return nil
	}
	current := interface{}(m)
	for _, key := range keys {
		cm, ok := current.(map[string]interface{})
		if !ok {
			return nil
		}
		current, ok = cm[key]
		if !ok {
			return nil
		}
	}
	return current
}

// jsonUint64 reads a top-level key from a flat map as uint64.
func jsonUint64(m map[string]interface{}, key string) uint64 {
	v, ok := m[key]
	if !ok {
		return 0
	}
	switch n := v.(type) {
	case float64:
		return uint64(n)
	case json.Number:
		if i, err := n.Int64(); err == nil {
			return uint64(i)
		}
	case string:
		if i, err := strconv.ParseUint(n, 10, 64); err == nil {
			return i
		}
	}
	return 0
}
