package health

import (
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"os"
	"time"
)

// certWarnDays is how close to expiry the certificate has to be before the
// status page complains. Let's Encrypt renews at 30 days left, so two weeks is
// comfortably past the point where automatic renewal should have happened.
const certWarnDays = 14

// Certificate is the state of the TLS certificate Postfix serves on 465/587
// (guide § Environment variables: TLS_CERT_FILE). The panel only reads it —
// the file is supplied by the reverse proxy through a read-only mount.
type Certificate struct {
	Path     string
	Subject  string
	NotAfter time.Time
	DaysLeft int
	Status   Status
	Detail   string
}

// CheckCertificate parses the leaf certificate at path and reports how much
// validity is left. A missing or unparsable file is an error status rather than
// an error return: the status page reports it in place, like every other check.
func CheckCertificate(path string) Certificate {
	c := Certificate{Path: path}
	if path == "" {
		c.Status = StatusUnknown
		c.Detail = "No certificate path is configured (TLS_CERT_FILE)."
		return c
	}
	data, err := os.ReadFile(path)
	if err != nil {
		c.Status = StatusError
		c.Detail = fmt.Sprintf("Could not read the certificate at %s.", path)
		return c
	}
	leaf, err := parseLeaf(data)
	if err != nil {
		c.Status = StatusError
		c.Detail = fmt.Sprintf("%s does not contain a readable certificate.", path)
		return c
	}

	c.Subject = leaf.Subject.CommonName
	c.NotAfter = leaf.NotAfter
	c.DaysLeft = int(time.Until(leaf.NotAfter).Hours() / 24)
	switch {
	case !time.Now().Before(leaf.NotAfter):
		c.Status = StatusError
		c.Detail = "The certificate has expired. Senders will refuse the TLS connection."
	case c.DaysLeft < certWarnDays:
		c.Status = StatusWarn
		c.Detail = fmt.Sprintf("Expires in %d day(s). Check that renewal on the host still works.", c.DaysLeft)
	default:
		c.Status = StatusOK
		c.Detail = fmt.Sprintf("Valid for another %d day(s).", c.DaysLeft)
	}
	return c
}

// parseLeaf returns the first certificate in a PEM chain — the leaf, which is
// the one whose validity clients see.
func parseLeaf(data []byte) (*x509.Certificate, error) {
	for rest := data; len(rest) > 0; {
		var block *pem.Block
		block, rest = pem.Decode(rest)
		if block == nil {
			break
		}
		if block.Type != "CERTIFICATE" {
			continue
		}
		return x509.ParseCertificate(block.Bytes)
	}
	return nil, fmt.Errorf("no CERTIFICATE block found")
}
