package main

import (
	"bytes"
	"crypto/rand"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// Pairing flow (agent side):
//
//  1. Fetch the controller's CA cert over HTTPS (with InsecureSkipVerify —
//     this is the only moment we're exposed; the one-time token gates
//     the actual cert issuance below).
//  2. Generate an RSA-2048 keypair locally. The private key never
//     leaves this host.
//  3. Build a CSR with CN = agent-id.
//  4. POST {token, agent_id, csr_pem} to /api/pair/issue, now verifying
//     the server cert against the CA we just fetched.
//  5. Persist ca.crt + agent.crt + agent.key to --cert-dir, mode 0600 on the key.
//
// After pairing, run the agent normally; it auto-detects the cert files
// and connects to the controller with mTLS.

type pairConfig struct {
	AgentID   string
	PairURL   string // e.g. https://controller:8080  — the HTTP dashboard port
	Token     string
	CertDir   string
	Logger    *slog.Logger
}

func runPair(cfg pairConfig) error {
	cfg.Logger.Info("pair: starting", "pair_url", cfg.PairURL, "agent_id", cfg.AgentID, "cert_dir", cfg.CertDir)

	if err := os.MkdirAll(cfg.CertDir, 0o755); err != nil {
		return fmt.Errorf("mkdir cert-dir: %w", err)
	}

	// Step 1 — fetch CA with InsecureSkipVerify, then verify with it.
	caPEM, err := fetchCA(cfg.PairURL, cfg.Logger)
	if err != nil {
		return fmt.Errorf("fetch CA: %w", err)
	}
	caPath := filepath.Join(cfg.CertDir, "ca.crt")
	if err := os.WriteFile(caPath, caPEM, 0o644); err != nil {
		return fmt.Errorf("write ca.crt: %w", err)
	}
	cfg.Logger.Info("pair: CA saved", "path", caPath, "bytes", len(caPEM))

	// Step 2 — keypair + CSR
	priv, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return fmt.Errorf("gen key: %w", err)
	}
	tpl := &x509.CertificateRequest{
		Subject:  pkix.Name{CommonName: cfg.AgentID, Organization: []string{"panel-agent"}},
		DNSNames: []string{cfg.AgentID},
	}
	csrDER, err := x509.CreateCertificateRequest(rand.Reader, tpl, priv)
	if err != nil {
		return fmt.Errorf("create CSR: %w", err)
	}
	csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER})

	// Step 3 — POST to /api/pair/issue, verifying CA.
	certPEM, err := issueCert(cfg.PairURL, caPEM, cfg.Token, cfg.AgentID, csrPEM, cfg.Logger)
	if err != nil {
		return fmt.Errorf("issue cert: %w", err)
	}

	// Step 4 — persist.
	if err := os.WriteFile(filepath.Join(cfg.CertDir, "agent.crt"), certPEM, 0o644); err != nil {
		return err
	}
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
	if err := os.WriteFile(filepath.Join(cfg.CertDir, "agent.key"), keyPEM, 0o600); err != nil {
		return err
	}
	cfg.Logger.Info("pair: cert issued", "cert_dir", cfg.CertDir, "agent_id", cfg.AgentID)
	return nil
}

func fetchCA(baseURL string, log *slog.Logger) ([]byte, error) {
	trimmed := strings.TrimSuffix(baseURL, "/")
	url := trimmed + "/api/pair/ca"
	log.Info("pair: fetching CA", "url", url)

	// Until we have the CA we can't verify the server. InsecureSkipVerify
	// is narrowly scoped to this one request — the cert-issuance POST
	// below verifies against the CA we just got.
	cli := &http.Client{
		Timeout: 15 * time.Second,
		Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
	}
	resp, err := cli.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		return nil, fmt.Errorf("http %d: %s", resp.StatusCode, string(body))
	}
	data, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
	if err != nil {
		return nil, err
	}
	block, _ := pem.Decode(data)
	if block == nil || block.Type != "CERTIFICATE" {
		return nil, fmt.Errorf("response isn't a CERTIFICATE PEM")
	}
	if _, err := x509.ParseCertificate(block.Bytes); err != nil {
		return nil, fmt.Errorf("parse CA cert: %w", err)
	}
	return data, nil
}

func issueCert(baseURL string, caPEM []byte, token, agentID string, csrPEM []byte, log *slog.Logger) ([]byte, error) {
	trimmed := strings.TrimSuffix(baseURL, "/")
	url := trimmed + "/api/pair/issue"
	log.Info("pair: requesting cert", "url", url, "agent_id", agentID)

	pool := x509.NewCertPool()
	if !pool.AppendCertsFromPEM(caPEM) {
		return nil, fmt.Errorf("append CA to pool failed")
	}
	cli := &http.Client{
		Timeout: 30 * time.Second,
		Transport: &http.Transport{TLSClientConfig: &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12}},
	}
	body, _ := json.Marshal(map[string]string{
		"token":    token,
		"agent_id": agentID,
		"csr_pem":  string(csrPEM),
	})
	req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := cli.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		errBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
		return nil, fmt.Errorf("http %d: %s", resp.StatusCode, string(errBody))
	}
	var out struct {
		CertPEM string `json:"cert_pem"`
		CAPEM   string `json:"ca_pem"`
		Subject string `json:"subject"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return nil, err
	}
	if out.CertPEM == "" {
		return nil, fmt.Errorf("response missing cert_pem")
	}
	return []byte(out.CertPEM), nil
}
