// Package probes implements the Checker port for each check kind.
// Each file exports a Checker constructor (e.g. NewHTTPChecker) that
// returns ports.Checker wrapping the scheduler's probe logic.
package probes

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"time"

	"github.com/dtoro/oikos/internal/core/ports"
)

// httpChecker is the HTTP reachability probe.
type httpChecker struct {
	client *http.Client
}

// NewHTTPChecker builds an HTTP probe with a connection-scoped client.
func NewHTTPChecker() ports.Checker {
	return &httpChecker{
		client: &http.Client{
			Timeout: 10 * time.Second,
			Transport: &http.Transport{
				TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
			},
		},
	}
}

func (c *httpChecker) Check(ctx context.Context, def ports.CheckDef, _ ports.Target) ports.CheckResult {
	cfg := configMap(def)

	url, _ := parseStr(cfg, "url")
	if url == "" {
		return ports.CheckResult{State: "unknown", Message: "no url in config"}
	}
	maxStatus := int(parseFloat(cfg, "max_status", 500))

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return ports.CheckResult{State: "unknown", Message: err.Error()}
	}

	resp, err := c.client.Do(req)
	if err != nil {
		return ports.CheckResult{State: "unknown", Message: err.Error()}
	}
	defer resp.Body.Close()
	io.Copy(io.Discard, resp.Body)

	code := resp.StatusCode
	if code > maxStatus {
		return ports.CheckResult{
			State: "critical", Value: float64(code),
			Message: fmt.Sprintf("HTTP %d exceeds max_status %d", code, maxStatus),
		}
	}
	return ports.CheckResult{State: "ok", Value: float64(code)}
}

// tcpChecker checks TCP port reachability.
type tcpChecker struct{}

func NewTCPChecker() ports.Checker { return &tcpChecker{} }

func (c *tcpChecker) Check(ctx context.Context, def ports.CheckDef, _ ports.Target) ports.CheckResult {
	cfg := configMap(def)

	host, _ := parseStr(cfg, "host")
	port := int(parseFloat(cfg, "port", 0))
	timeout := time.Duration(parseFloat(cfg, "timeout", 5)) * time.Second

	addr := fmt.Sprintf("%s:%d", host, port)
	d := &net.Dialer{Timeout: timeout}
	conn, err := d.DialContext(ctx, "tcp", addr)
	if err != nil {
		return ports.CheckResult{State: "critical", Message: err.Error()}
	}
	conn.Close()
	return ports.CheckResult{State: "ok", Value: float64(port)}
}

// pingChecker is the ICMP/connectivity probe. Falls back to TCP ping on
// systems without raw socket access.
type pingChecker struct{}

func NewPingChecker() ports.Checker { return &pingChecker{} }

func (c *pingChecker) Check(ctx context.Context, def ports.CheckDef, _ ports.Target) ports.CheckResult {
	cfg := configMap(def)

	host, _ := parseStr(cfg, "host")
	if host == "" {
		return ports.CheckResult{State: "unknown", Message: "no host in config"}
	}
	port := int(parseFloat(cfg, "port", 80))
	timeout := time.Duration(parseFloat(cfg, "timeout", 5)) * time.Second

	d := &net.Dialer{Timeout: timeout}
	addr := fmt.Sprintf("%s:%d", host, port)
	conn, err := d.DialContext(ctx, "tcp", addr)
	if err != nil {
		return ports.CheckResult{State: "critical", Message: err.Error(), Value: -1}
	}
	conn.Close()
	return ports.CheckResult{State: "ok", Value: 0}
}

// dnsChecker resolves DNS names.
type dnsChecker struct{}

func NewDNSChecker() ports.Checker { return &dnsChecker{} }

func (c *dnsChecker) Check(ctx context.Context, def ports.CheckDef, _ ports.Target) ports.CheckResult {
	cfg := configMap(def)

	name, _ := parseStr(cfg, "name")
	if name == "" {
		return ports.CheckResult{State: "unknown", Message: "no name in config"}
	}
	var r net.Resolver
	addrs, err := r.LookupHost(ctx, name)
	if err != nil {
		return ports.CheckResult{State: "critical", Message: err.Error()}
	}
	return ports.CheckResult{State: "ok", Value: float64(len(addrs))}
}


// configMap unmarshals a check's JSON config into a map.
func configMap(def ports.CheckDef) map[string]any {
	var m map[string]any
	if len(def.Config) > 0 {
		json.Unmarshal(def.Config, &m)
	}
	if m == nil {
		m = map[string]any{}
	}
	return m
}
func parseStr(m map[string]any, key string) (string, bool) {
	v, ok := m[key]
	if !ok {
		return "", false
	}
	s, ok := v.(string)
	return s, ok
}

func parseFloat(m map[string]any, key string, def float64) float64 {
	v, ok := m[key]
	if !ok {
		return def
	}
	switch n := v.(type) {
	case float64:
		return n
	case int:
		return float64(n)
	case json.Number:
		f, _ := n.Float64()
		return f
	}
	return def
}