// Independent replication of the PQC surface probe.
// Input (stdin): JSON lines {"domain":..,"host":..,"kind":"https"|"smtp"}.
// For each door, four TLS handshakes: hybrid-only (X25519MLKEM768, TLS 1.3),
// X25519-only (TLS 1.3), P-256-only (TLS 1.3) and TLS 1.2 (default suites).
// Output (stdout): one JSON line per door.
package main

import (
	"bufio"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"net"
	"net/textproto"
	"os"
	"strings"
	"sync"
	"time"
)

type door struct {
	Domain string `json:"domain"`
	Host   string `json:"host"`
	Kind   string `json:"kind"`
}

type mode struct {
	Name  string
	Min   uint16
	Max   uint16
	Curve []tls.CurveID
}

var modes = []mode{
	{"pq", tls.VersionTLS13, tls.VersionTLS13, []tls.CurveID{tls.X25519MLKEM768}},
	{"x25519", tls.VersionTLS13, tls.VersionTLS13, []tls.CurveID{tls.X25519}},
	{"p256", tls.VersionTLS13, tls.VersionTLS13, []tls.CurveID{tls.CurveP256}},
	{"tls12", tls.VersionTLS12, tls.VersionTLS12, nil},
}

type out struct {
	door
	IP      string            `json:"ip"`
	Status  map[string]string `json:"status"`
	Curve   string            `json:"pq_curve,omitempty"`
	Verdict string            `json:"verdict"`
	At      string            `json:"at"`
}

func dialTLS(d door, ip string, m mode) (string, string) {
	cfg := &tls.Config{ServerName: d.Host, MinVersion: m.Min, MaxVersion: m.Max, CurvePreferences: m.Curve, InsecureSkipVerify: true}
	dl := &net.Dialer{Timeout: 7 * time.Second}
	var conn net.Conn
	var err error
	if d.Kind == "smtp" {
		conn, err = dl.Dial("tcp", net.JoinHostPort(ip, "25"))
		if err != nil {
			return classify(err), ""
		}
		conn.SetDeadline(time.Now().Add(15 * time.Second))
		tp := textproto.NewConn(conn)
		if _, _, err = tp.ReadResponse(220); err != nil {
			conn.Close()
			return "smtp-banner", ""
		}
		tp.PrintfLine("EHLO pq-replication.invalid")
		if _, _, err = tp.ReadResponse(250); err != nil {
			conn.Close()
			return "smtp-ehlo", ""
		}
		tp.PrintfLine("STARTTLS")
		if _, _, err = tp.ReadResponse(220); err != nil {
			conn.Close()
			return "smtp-nostarttls", ""
		}
	} else {
		conn, err = dl.Dial("tcp", net.JoinHostPort(ip, "443"))
		if err != nil {
			return classify(err), ""
		}
		conn.SetDeadline(time.Now().Add(15 * time.Second))
	}
	tc := tls.Client(conn, cfg)
	defer tc.Close()
	if err := tc.Handshake(); err != nil {
		return classify(err), ""
	}
	cs := tc.ConnectionState()
	if m.Name == "pq" && cs.CurveID != tls.X25519MLKEM768 {
		return "wrong-curve", fmt.Sprint(cs.CurveID)
	}
	return "ok", fmt.Sprint(cs.CurveID)
}

func classify(err error) string {
	s := err.Error()
	switch {
	case strings.Contains(s, "timeout"):
		return "timeout"
	case strings.Contains(s, "connection refused"):
		return "refused"
	case strings.Contains(s, "no route") || strings.Contains(s, "unreachable"):
		return "unreachable"
	case strings.Contains(s, "reset"):
		return "reset"
	case strings.Contains(s, "remote error") || strings.Contains(s, "handshake failure") || strings.Contains(s, "protocol version") || strings.Contains(s, "EOF"):
		return "rejected"
	}
	return "error"
}

func verdict(st map[string]string) string {
	pq := st["pq"] == "ok"
	cl := st["x25519"] == "ok" || st["p256"] == "ok" || st["tls12"] == "ok"
	switch {
	case pq && !cl:
		return "clean"
	case pq && cl:
		return "hybrid_exposed"
	case cl:
		return "classical"
	}
	return "unmeasured"
}

func main() {
	var doors []door
	sc := bufio.NewScanner(os.Stdin)
	for sc.Scan() {
		var d door
		if json.Unmarshal(sc.Bytes(), &d) == nil && d.Host != "" {
			doors = append(doors, d)
		}
	}
	jobs := make(chan door)
	var mu sync.Mutex
	enc := json.NewEncoder(os.Stdout)
	var wg sync.WaitGroup
	for i := 0; i < 48; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for d := range jobs {
				o := out{door: d, Status: map[string]string{}, At: time.Now().UTC().Format(time.RFC3339)}
				ips, err := net.LookupHost(d.Host)
				if err != nil || len(ips) == 0 {
					o.Verdict = "unresolved"
				} else {
					o.IP = ips[0]
					for _, m := range modes {
						st, curve := dialTLS(d, o.IP, m)
						o.Status[m.Name] = st
						if m.Name == "pq" && st == "ok" {
							o.Curve = curve
						}
					}
					o.Verdict = verdict(o.Status)
				}
				mu.Lock()
				enc.Encode(o)
				mu.Unlock()
			}
		}()
	}
	for _, d := range doors {
		jobs <- d
	}
	close(jobs)
	wg.Wait()
}
