﻿package engine

import (
	"crypto/ecdsa"
	"crypto/ed25519"
	"crypto/rsa"
	"crypto/tls"
	"fmt"
	"log/slog"
	"math"
	"os"
	"strings"
	"time"

	"github.com/go-acme/lego/v4/certcrypto"
	"github.com/go-resty/resty/v2"

	"github.com/certimate-go/certimate/internal/domain"
	"github.com/certimate-go/certimate/internal/repository"
	xcert "github.com/certimate-go/certimate/pkg/utils/cert"
)

/**
 * Outputs:
 *   - ref: "certificate": string
 *
 * Variables:
 *   - "node.skipped": boolean
 *   - "certificate.domain": string
 *   - "certificate.domains": string
 *   - "certificate.notBefore": datetime
 *   - "certificate.notAfter": datetime
 *   - "certificate.hoursLeft": number
 *   - "certificate.daysLeft": number
 *   - "certificate.validity": boolean
 */
type bizUploadNodeExecutor struct {
	nodeExecutor

	certificateRepo certificateRepository
	wfoutputRepo    workflowOutputRepository
}

const (
	BizUploadSourceForm  = "form"
	BizUploadSourceLocal = "local"
	BizUploadSourceURL   = "url"
)

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

	nodeCfg := execCtx.Node.Data.Config.AsBizUpload()
	ne.logger.Info("ready to upload certiticate ...", slog.Any("config", nodeCfg))

	// 查询上次执行结果
	lastOutput, lastCertificate, err := ne.getLastOutputArtifacts(execCtx)
	if err != nil {
		return execRes, err
	} else if lastCertificate != nil {
		ne.setOuputsOfResult(execCtx, execRes, lastCertificate, false)
		ne.setVariablesOfResult(execCtx, execRes, lastCertificate)
	}

	// 检测是否可以跳过本次执行
	if skippable, reason := ne.checkCanSkip(execCtx, lastOutput, lastCertificate); skippable {
		ne.logger.Info(fmt.Sprintf("skip this uploading, because %s", reason))

		execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, true, "boolean")
		return execRes, nil
	} else if reason != "" {
		ne.logger.Info(fmt.Sprintf("re-upload, because %s", reason))
	} else if lastCertificate != nil {
		ne.logger.Info("no found last uploaded certificate, begin to upload")
	} else {
		ne.logger.Info("try to upload")
	}

	// 获取证书及私钥
	var certPEM, privkeyPEM string
	switch nodeCfg.Source {
	case BizUploadSourceForm:
		{
			certPEM = nodeCfg.Certificate
			privkeyPEM = nodeCfg.PrivateKey
		}

	case BizUploadSourceLocal:
		{
			certData, err := os.ReadFile(nodeCfg.Certificate)
			if err != nil {
				return execRes, fmt.Errorf("failed to read certificate file from local path: %w", err)
			} else {
				certPEM = string(certData)
			}

			privkeyData, err := os.ReadFile(nodeCfg.PrivateKey)
			if err != nil {
				return execRes, fmt.Errorf("failed to read private key file from local path: %w", err)
			} else {
				privkeyPEM = string(privkeyData)
			}
		}

	case BizUploadSourceURL:
		{
			client := resty.New()
			client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})

			certResp, err := client.NewRequest().Get(nodeCfg.Certificate)
			if err != nil || certResp.IsError() {
				return execRes, fmt.Errorf("failed to download certificate from URL: %w", err)
			} else {
				certPEM = string(certResp.Body())
			}

			privkeyResp, err := client.NewRequest().Get(nodeCfg.PrivateKey)
			if err != nil || privkeyResp.IsError() {
				return execRes, fmt.Errorf("failed to download private key from URL: %w", err)
			} else {
				privkeyPEM = string(privkeyResp.Body())
			}
		}

	default:
		return execRes, fmt.Errorf("unsupported upload source: '%s'", nodeCfg.Source)
	}

	// 验证证书
	certX509, err := certcrypto.ParsePEMCertificate([]byte(certPEM))
	if err != nil {
		return execRes, err
	} else if certX509.NotAfter.Before(time.Now()) {
		ne.logger.Warn(fmt.Sprintf("the uploaded certificate has expired at %s", certX509.NotAfter.UTC().Format(time.RFC3339)))
	}

	// 验证私钥
	privkey, err := certcrypto.ParsePEMPrivateKey([]byte(privkeyPEM))
	if err != nil {
		return nil, err
	} else {
		matched := false
		switch pub := certX509.PublicKey.(type) {
		case *rsa.PublicKey:
			p, ok := privkey.(*rsa.PrivateKey)
			matched = ok && pub.Equal(p.Public())
		case *ecdsa.PublicKey:
			p, ok := privkey.(*ecdsa.PrivateKey)
			matched = ok && pub.Equal(p.Public())
		case ed25519.PublicKey:
			p, ok := privkey.(ed25519.PrivateKey)
			matched = ok && pub.Equal(p.Public())
		default:
			matched = false
		}

		if !matched {
			return nil, fmt.Errorf("the uploaded private key does not match the uploaded certificate")
		}
	}

	// 二次检测是否可以跳过执行
	if lastCertificate != nil {
		lastCertX509, err := xcert.ParseCertificateFromPEM(lastCertificate.Certificate)
		if err == nil && xcert.EqualCertificates(certX509, lastCertX509) {
			ne.logger.Info("skip this uploading, because the last uploaded certificate already exists")
			return execRes, nil
		}
	}

	// 保存证书实体
	certificate := &domain.Certificate{
		Source:         domain.CertificateSourceTypeUpload,
		WorkflowId:     execCtx.WorkflowId,
		WorkflowRunId:  execCtx.RunId,
		WorkflowNodeId: execCtx.Node.Id,
	}
	certificate.PopulateFromPEM(certPEM, privkeyPEM)
	if certificate, err := ne.certificateRepo.Save(execCtx.ctx, certificate); err != nil {
		ne.logger.Warn("could not save certificate")
		return execRes, err
	} else {
		ne.logger.Info("certificate saved", slog.String("recordId", certificate.Id))
	}

	// 节点输出
	ne.setOuputsOfResult(execCtx, execRes, certificate, true)
	ne.setVariablesOfResult(execCtx, execRes, certificate)

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

func (ne *bizUploadNodeExecutor) getLastOutputArtifacts(execCtx *NodeExecutionContext) (*domain.WorkflowOutput, *domain.Certificate, error) {
	lastOutput, err := ne.wfoutputRepo.GetByNodeId(execCtx.ctx, execCtx.Node.Id)
	if err != nil && !domain.IsRecordNotFoundError(err) {
		return nil, nil, fmt.Errorf("failed to get last output record of node #%s: %w", execCtx.Node.Id, err)
	}

	if lastOutput != nil {
		lastCertificate, err := ne.certificateRepo.GetByWorkflowRunIdAndNodeId(execCtx.ctx, lastOutput.RunId, lastOutput.NodeId)
		if err != nil && !domain.IsRecordNotFoundError(err) {
			return lastOutput, nil, fmt.Errorf("failed to get last certificate record of node #%s: %w", execCtx.Node.Id, err)
		}

		return lastOutput, lastCertificate, nil
	}

	return lastOutput, nil, nil
}

func (ne *bizUploadNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext, lastOutput *domain.WorkflowOutput, lastCertificate *domain.Certificate) (_skip bool, _reason string) {
	thisNodeCfg := execCtx.Node.Data.Config.AsBizUpload()

	if lastOutput != nil && lastOutput.Succeeded {
		// 比较和上次上传时的关键配置（即影响证书上传的）参数是否一致
		lastNodeCfg := lastOutput.NodeConfig.AsBizUpload()

		if thisNodeCfg.Source != lastNodeCfg.Source {
			return false, "the configuration item 'Source' changed"
		}

		switch thisNodeCfg.Source {
		case BizUploadSourceForm:
			if strings.TrimSpace(thisNodeCfg.Certificate) != strings.TrimSpace(lastNodeCfg.Certificate) {
				return false, "the configuration item 'Certificate' changed"
			}
			if strings.TrimSpace(thisNodeCfg.PrivateKey) != strings.TrimSpace(lastNodeCfg.PrivateKey) {
				return false, "the configuration item 'PrivateKey' changed"
			}

		default:
			// 本地或远程文件来源，需实际下载后才能比较
			return false, ""
		}
	}

	if lastCertificate != nil {
		return true, "the last uploaded certificate already exists"
	}

	return false, ""
}

func (ne *bizUploadNodeExecutor) setOuputsOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certificate *domain.Certificate, persistent bool) {
	if certificate != nil {
		key := "certificate"
		value := fmt.Sprintf("%s#%s", domain.CollectionNameCertificate, certificate.Id)
		if persistent {
			execRes.AddOutputWithPersistent(stateIOTypeRef, key, value, "string")
		} else {
			execRes.AddOutput(stateIOTypeRef, key, value, "string")
		}
	}
}

func (ne *bizUploadNodeExecutor) setVariablesOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certificate *domain.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 certificate != nil {
		vDomain = strings.Split(certificate.SubjectAltNames, ";")[0]
		vDomains = certificate.SubjectAltNames
		vNotBefore = certificate.ValidityNotBefore
		vNotAfter = certificate.ValidityNotAfter
		vHoursLeft = int32(math.Floor(time.Until(certificate.ValidityNotAfter).Hours()))
		vDaysLeft = int32(math.Floor(time.Until(certificate.ValidityNotAfter).Hours() / 24))
		vValidity = certificate.ValidityNotAfter.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 newBizUploadNodeExecutor() NodeExecutor {
	return &bizUploadNodeExecutor{
		nodeExecutor:    nodeExecutor{logger: slog.Default()},
		certificateRepo: repository.NewCertificateRepository(),
		wfoutputRepo:    repository.NewWorkflowOutputRepository(),
	}
}
