﻿package engine

import (
	"crypto/x509"
	"fmt"
	"log/slog"
	"math"
	"net"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/certimate-go/certimate/internal/repository"
	xhttp "github.com/certimate-go/certimate/pkg/utils/http"
	xtls "github.com/certimate-go/certimate/pkg/utils/tls"
)

/**
 * Variables:
 *   - "certificate.domain": string
 *   - "certificate.domains": string
 *   - "certificate.notBefore": datetime
 *   - "certificate.notAfter": datetime
 *   - "certificate.hoursLeft": number
 *   - "certificate.daysLeft": number
 *   - "certificate.validity": boolean
 */
type bizMonitorNodeExecutor struct {
	nodeExecutor

	certificateRepo certificateRepository
}

func (ne *bizMonitorNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {
	execRes := newNodeExecutionResult(execCtx.Node)

	nodeCfg := execCtx.Node.Data.Config.AsBizMonitor()
	ne.logger.Info("ready to monitor certificate ...", slog.Any("config", nodeCfg))

	targetAddr := net.JoinHostPort(nodeCfg.Host, strconv.Itoa(int(nodeCfg.Port)))
	if nodeCfg.Port == 0 {
		targetAddr = net.JoinHostPort(nodeCfg.Host, "443")
	}

	targetDomain := nodeCfg.Domain
	if targetDomain == "" {
		targetDomain = nodeCfg.Host
	}

	ne.logger.Info(fmt.Sprintf("retrieving certificate at %s (domain: %s)", targetAddr, targetDomain))

	const MAX_ATTEMPTS = 3
	const RETRY_INTERVAL = 2 * time.Second
	var err error
	var certs []*x509.Certificate
	for attempt := 0; attempt < MAX_ATTEMPTS; attempt++ {
		if attempt > 0 {
			ne.logger.Info(fmt.Sprintf("retry %d time(s) ...", attempt))

			select {
			case <-execCtx.ctx.Done():
				return execRes, execCtx.ctx.Err()
			case <-time.After(RETRY_INTERVAL):
			}
		}

		certs, err = ne.tryRetrievePeerCertificates(execCtx, targetAddr, targetDomain, nodeCfg.RequestPath)
		if err == nil {
			break
		}
	}

	if err != nil {
		ne.logger.Warn("could not retrieve certificate")
		return execRes, err
	} else {
		if len(certs) == 0 {
			ne.logger.Warn("no ssl certificates retrieved in http response")

			ne.setVariablesOfResult(execCtx, execRes, nil)
		} else {
			cert := certs[0] // 只取证书链中的第一个证书，即服务器证书
			ne.logger.Info(fmt.Sprintf("ssl certificate retrieved (serial='%s', subject='%s', issuer='%s', not_before='%s', not_after='%s', sans='%s')",
				cert.SerialNumber, cert.Subject.String(), cert.Issuer.String(),
				cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339),
				strings.Join(cert.DNSNames, ";")),
			)
			ne.setVariablesOfResult(execCtx, execRes, cert)

			now := time.Now()
			isCertPeriodValid := now.Before(cert.NotAfter) && now.After(cert.NotBefore)
			isCertHostMatched := cert.VerifyHostname(targetDomain) == nil
			daysLeft := int32(math.Floor(time.Until(cert.NotAfter).Hours() / 24))
			validated := isCertPeriodValid && isCertHostMatched

			if validated {
				ne.logger.Info(fmt.Sprintf("the certificate is valid, and will expire in %d day(s)", daysLeft))
			} else {
				if !isCertHostMatched {
					ne.logger.Warn("the certificate is invalid, because it is not matched the host")
				} else if !isCertPeriodValid {
					ne.logger.Warn("the certificate is invalid, because it is either expired or not yet valid")
				} else {
					ne.logger.Warn("the certificate is invalid")
				}

				// 除了验证证书有效期，还要确保证书与域名匹配
				execRes.AddVariable(stateVarKeyCertificateValidity, false, "boolean")
				execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, false, "boolean")
			}
		}
	}

	ne.logger.Info("monitoring completed")
	return execRes, nil
}

func (ne *bizMonitorNodeExecutor) tryRetrievePeerCertificates(execCtx *NodeExecutionContext, addr, domain, requestPath string) ([]*x509.Certificate, error) {
	transport := xhttp.NewDefaultTransport()
	transport.TLSClientConfig = xtls.NewInsecureConfig()

	client := &http.Client{
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
		Timeout:   30 * time.Second,
		Transport: transport,
	}

	url := fmt.Sprintf("https://%s/%s", addr, strings.TrimLeft(requestPath, "/"))
	req, err := http.NewRequestWithContext(execCtx.ctx, http.MethodHead, url, nil)
	if err != nil {
		err = fmt.Errorf("failed to create http request: %w", err)
		ne.logger.Warn(err.Error())
		return nil, err
	}

	req.Header.Set("Host", domain)
	req.Header.Set("User-Agent", "certimate")
	resp, err := client.Do(req)
	if err != nil {
		err = fmt.Errorf("failed to send http request: %w", err)
		ne.logger.Warn(err.Error())
		return nil, err
	}
	defer resp.Body.Close()

	if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {
		return make([]*x509.Certificate, 0), nil
	}
	return resp.TLS.PeerCertificates, nil
}

func (ne *bizMonitorNodeExecutor) setVariablesOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certX509 *x509.Certificate) {
	var vDomain string
	var vDomains string
	var vNotBefore time.Time
	var vNotAfter time.Time
	var vHoursLeft int32
	var vDaysLeft int32
	var vValidity bool

	if certX509 != nil {
		vDomain = certX509.Subject.CommonName
		vDomains = strings.Join(certX509.DNSNames, ";")
		vNotBefore = certX509.NotBefore
		vNotAfter = certX509.NotAfter
		vHoursLeft = int32(math.Floor(time.Until(certX509.NotAfter).Hours()))
		vDaysLeft = int32(math.Floor(time.Until(certX509.NotAfter).Hours() / 24))
		vValidity = certX509.NotAfter.After(time.Now())
	}

	execRes.AddVariable(stateVarKeyCertificateDomain, vDomain, "string")
	execRes.AddVariable(stateVarKeyCertificateDomains, vDomains, "string")
	execRes.AddVariable(stateVarKeyCertificateNotBefore, vNotBefore, "datetime")
	execRes.AddVariable(stateVarKeyCertificateNotAfter, vNotAfter, "datetime")
	execRes.AddVariable(stateVarKeyCertificateHoursLeft, vHoursLeft, "number")
	execRes.AddVariable(stateVarKeyCertificateDaysLeft, vDaysLeft, "number")
	execRes.AddVariable(stateVarKeyCertificateValidity, vValidity, "boolean")
	execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomain, vDomain, "string")
	execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomains, vDomains, "string")
	execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotBefore, vNotBefore, "datetime")
	execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotAfter, vNotAfter, "datetime")
	execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateHoursLeft, vHoursLeft, "number")
	execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, vDaysLeft, "number")
	execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, vValidity, "boolean")
}

func newBizMonitorNodeExecutor() NodeExecutor {
	return &bizMonitorNodeExecutor{
		nodeExecutor:    nodeExecutor{logger: slog.Default()},
		certificateRepo: repository.NewCertificateRepository(),
	}
}
