package checker

import (
	"context"
	"crypto/tls"
	"fmt"
	"io"
	"net"
	"net/http"
	"os"
	"strings"
	"time"
)


const (
	TypeHTTP = "http"
	TypeTCP  = "tcp"
	TypeICMP = "icmp"
	TypeTLS  = "tls"
)


type Spec struct {
	Type           string
	URL            string
	Host           string
	Port           int
	Method         string
	ExpectedStatus int
	Keyword        string
	TLSWarnDays    int
	Insecure       bool 
	TimeoutSec     int
}


type Result struct {
	Up         bool
	LatencyMs  int
	Message    string
	CertExpiry *time.Time
}

func (s Spec) timeout() time.Duration {
	t := s.TimeoutSec
	if t <= 0 || t > 10 {
		t = 10
	}
	return time.Duration(t) * time.Second
}


func Run(ctx context.Context, s Spec) Result {
	switch s.Type {
	case TypeHTTP:
		return runHTTP(ctx, s)
	case TypeTCP:
		return runTCP(ctx, s)
	case TypeICMP:
		return runICMP(ctx, s)
	case TypeTLS:
		return runTLS(ctx, s)
	default:
		return Result{Message: "unknown check type: " + s.Type}
	}
}

func runHTTP(ctx context.Context, s Spec) Result {
	method := s.Method
	if method == "" {
		method = http.MethodGet
	}
	expect := s.ExpectedStatus
	if expect == 0 {
		expect = 200
	}
	client := &http.Client{Timeout: s.timeout()}
	if s.Insecure {
		client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} 
	}
	start := time.Now()
	req, err := http.NewRequestWithContext(ctx, method, s.URL, nil)
	if err != nil {
		return Result{Message: err.Error()}
	}
	resp, err := client.Do(req)
	if err != nil {
		return Result{LatencyMs: msSince(start), Message: err.Error()}
	}
	defer resp.Body.Close()
	res := Result{LatencyMs: msSince(start), Up: true}
	if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 {
		exp := resp.TLS.PeerCertificates[0].NotAfter
		res.CertExpiry = &exp
	}
	if resp.StatusCode != expect {
		return Result{LatencyMs: res.LatencyMs, CertExpiry: res.CertExpiry, Message: fmt.Sprintf("status %d (want %d)", resp.StatusCode, expect)}
	}
	if s.Keyword != "" {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
		if !strings.Contains(string(body), s.Keyword) {
			return Result{LatencyMs: res.LatencyMs, CertExpiry: res.CertExpiry, Message: "keyword not found"}
		}
	}
	return res
}

func runTCP(ctx context.Context, s Spec) Result {
	addr := net.JoinHostPort(s.Host, fmt.Sprint(s.Port))
	start := time.Now()
	d := net.Dialer{Timeout: s.timeout()}
	conn, err := d.DialContext(ctx, "tcp", addr)
	if err != nil {
		return Result{LatencyMs: msSince(start), Message: err.Error()}
	}
	conn.Close()
	return Result{Up: true, LatencyMs: msSince(start)}
}

func runTLS(ctx context.Context, s Spec) Result {
	port := s.Port
	if port == 0 {
		port = 443
	}
	addr := net.JoinHostPort(s.Host, fmt.Sprint(port))
	start := time.Now()
	d := net.Dialer{Timeout: s.timeout()}
	conn, err := tls.DialWithDialer(&d, "tcp", addr, &tls.Config{ServerName: s.Host})
	if err != nil {
		return Result{LatencyMs: msSince(start), Message: err.Error()}
	}
	defer conn.Close()
	certs := conn.ConnectionState().PeerCertificates
	if len(certs) == 0 {
		return Result{LatencyMs: msSince(start), Message: "no peer certificate"}
	}
	exp := certs[0].NotAfter
	res := Result{LatencyMs: msSince(start), CertExpiry: &exp}
	warn := s.TLSWarnDays
	if warn <= 0 {
		warn = 14
	}
	remaining := time.Until(exp)
	if remaining <= 0 {
		res.Message = "certificate expired"
		return res
	}
	if remaining <= time.Duration(warn)*24*time.Hour {
		res.Message = fmt.Sprintf("certificate expires in %d days", int(remaining.Hours()/24))
		return res
	}
	res.Up = true
	return res
}

func msSince(t time.Time) int { return int(time.Since(t).Milliseconds()) }




func runICMP(ctx context.Context, s Spec) Result {
	dst, err := net.ResolveIPAddr("ip4", s.Host)
	if err != nil {
		return Result{Message: err.Error()}
	}
	conn, err := net.ListenPacket("ip4:icmp", "0.0.0.0")
	if err != nil {
		return Result{Message: "icmp socket: " + err.Error()}
	}
	defer conn.Close()

	id := os.Getpid() & 0xffff
	pkt := icmpEcho(id, 1)
	deadline := time.Now().Add(s.timeout())
	if d, ok := ctx.Deadline(); ok && d.Before(deadline) {
		deadline = d
	}
	_ = conn.SetDeadline(deadline)

	start := time.Now()
	if _, err := conn.WriteTo(pkt, dst); err != nil {
		return Result{Message: err.Error()}
	}
	reply := make([]byte, 1500)
	for {
		n, peer, err := conn.ReadFrom(reply)
		if err != nil {
			return Result{LatencyMs: msSince(start), Message: "no reply"}
		}
		
		if n < 28 || peer.String() != dst.String() {
			continue
		}
		if reply[20] == 0 { 
			return Result{Up: true, LatencyMs: msSince(start)}
		}
	}
}

func icmpEcho(id, seq int) []byte {
	
	b := []byte{8, 0, 0, 0, byte(id >> 8), byte(id), byte(seq >> 8), byte(seq)}
	cs := icmpChecksum(b)
	b[2] = byte(cs >> 8)
	b[3] = byte(cs)
	return b
}

func icmpChecksum(b []byte) uint16 {
	var sum uint32
	for i := 0; i < len(b)-1; i += 2 {
		sum += uint32(b[i])<<8 | uint32(b[i+1])
	}
	if len(b)%2 == 1 {
		sum += uint32(b[len(b)-1]) << 8
	}
	for sum>>16 != 0 {
		sum = (sum & 0xffff) + (sum >> 16)
	}
	return ^uint16(sum)
}
