package agentd

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"errors"
	"fmt"
	"io/fs"
	"os"

	agentv1 "github.com/wangjia/pangolin/server/internal/pb/agentv1"
	"google.golang.org/grpc"
)

// EnrollDialer opens a connection suitable for the (un-authenticated) Enroll RPC.
// The agent has no client certificate yet, so this connection presents none; the
// bootstrap token authenticates the call instead.
type EnrollDialer func(ctx context.Context) (*grpc.ClientConn, error)

// EnsureEnrolled guarantees the node holds a valid client certificate.
//
//   - If node.crt/node.key already exist, it loads and returns the certificate's
//     CN (the node UUID) without contacting the control plane (idempotent first
//     start vs. restart).
//   - Otherwise it generates an EC P-256 key + CSR, calls Enroll with the
//     bootstrap token, and persists node.key (0600), node.crt and ca.crt.
//
// The private key is generated on the node and never leaves it.
func EnsureEnrolled(ctx context.Context, cfg Config, dial EnrollDialer) (string, error) {
	cfg = cfg.withDefaults()

	if uuid, ok, err := loadEnrolledUUID(cfg); err != nil {
		return "", err
	} else if ok {
		return uuid, nil
	}

	if cfg.BootstrapToken == "" {
		return "", errors.New("agentd: not enrolled and no bootstrap token provided")
	}

	keyPEM, csrPEM, err := generateKeyAndCSR()
	if err != nil {
		return "", err
	}

	conn, err := dial(ctx)
	if err != nil {
		return "", fmt.Errorf("agentd: dial for enroll: %w", err)
	}
	defer conn.Close()

	client := agentv1.NewAgentServiceClient(conn)
	resp, err := client.Enroll(ctx, &agentv1.EnrollRequest{
		BootstrapToken: cfg.BootstrapToken,
		CSRPEM:         csrPEM,
		AgentVersion:   cfg.AgentVersion,
	})
	if err != nil {
		return "", fmt.Errorf("agentd: enroll rpc: %w", err)
	}
	if resp.NodeUUID == "" || len(resp.CertPEM) == 0 {
		return "", errors.New("agentd: enroll response missing node_uuid/cert")
	}

	if err := os.MkdirAll(cfg.StateDir, 0o700); err != nil {
		return "", fmt.Errorf("agentd: mkdir state dir: %w", err)
	}
	if err := atomicWrite(cfg.KeyPath(), keyPEM, 0o600); err != nil {
		return "", err
	}
	if err := atomicWrite(cfg.CertPath(), resp.CertPEM, 0o644); err != nil {
		return "", err
	}
	if len(resp.CAPEM) > 0 {
		if err := atomicWrite(cfg.CAPath(), resp.CAPEM, 0o644); err != nil {
			return "", err
		}
	}
	logf("enrolled successfully (cert valid until unix=%d)", resp.NotAfterUnix)
	return resp.NodeUUID, nil
}

// loadEnrolledUUID returns the CN of the persisted node certificate, if present.
func loadEnrolledUUID(cfg Config) (string, bool, error) {
	certPEM, err := os.ReadFile(cfg.CertPath())
	if errors.Is(err, fs.ErrNotExist) {
		return "", false, nil
	}
	if err != nil {
		return "", false, fmt.Errorf("agentd: read node cert: %w", err)
	}
	if _, err := os.Stat(cfg.KeyPath()); err != nil {
		// Cert without key is unusable — treat as not enrolled.
		return "", false, nil
	}
	block, _ := pem.Decode(certPEM)
	if block == nil {
		return "", false, errors.New("agentd: node cert has no PEM block")
	}
	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		return "", false, fmt.Errorf("agentd: parse node cert: %w", err)
	}
	if cert.Subject.CommonName == "" {
		return "", false, errors.New("agentd: node cert has empty CN")
	}
	return cert.Subject.CommonName, true, nil
}

// generateKeyAndCSR creates an EC P-256 private key and a PKCS#10 CSR. The CSR's
// CN is a placeholder; the CA overrides it with the authoritative node UUID.
func generateKeyAndCSR() (keyPEM, csrPEM []byte, err error) {
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return nil, nil, fmt.Errorf("agentd: generate key: %w", err)
	}
	keyDER, err := x509.MarshalECPrivateKey(key)
	if err != nil {
		return nil, nil, fmt.Errorf("agentd: marshal key: %w", err)
	}
	keyPEM = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})

	tmpl := &x509.CertificateRequest{
		Subject:            pkix.Name{CommonName: "pangolin-node-pending"},
		SignatureAlgorithm: x509.ECDSAWithSHA256,
	}
	csrDER, err := x509.CreateCertificateRequest(rand.Reader, tmpl, key)
	if err != nil {
		return nil, nil, fmt.Errorf("agentd: create CSR: %w", err)
	}
	csrPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER})
	return keyPEM, csrPEM, nil
}
