package service

import (
	"context"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"io"
	"log"
	"log/slog"
	"os"
	"path"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/go-acme/lego/v5/certificate"
	legoLogger "github.com/go-acme/lego/v5/log"

	"github.com/1Panel-dev/1Panel/agent/app/dto/request"
	"github.com/1Panel-dev/1Panel/agent/app/dto/response"
	"github.com/1Panel-dev/1Panel/agent/app/model"
	"github.com/1Panel-dev/1Panel/agent/app/repo"
	"github.com/1Panel-dev/1Panel/agent/app/task"
	"github.com/1Panel-dev/1Panel/agent/buserr"
	"github.com/1Panel-dev/1Panel/agent/constant"
	"github.com/1Panel-dev/1Panel/agent/global"
	"github.com/1Panel-dev/1Panel/agent/i18n"
	"github.com/1Panel-dev/1Panel/agent/utils/cmd"
	"github.com/1Panel-dev/1Panel/agent/utils/common"
	"github.com/1Panel-dev/1Panel/agent/utils/files"
	"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
	"github.com/1Panel-dev/1Panel/agent/utils/ssl"
	"github.com/1Panel-dev/1Panel/agent/utils/xpack"
	gormv2 "gorm.io/gorm"
)

type WebsiteSSLService struct {
}

const sslObtainTimeout = time.Hour

var legoLogMu sync.Mutex

func withLegoLogger(logger *log.Logger, fn func() error) error {
	if logger == nil {
		logger = log.New(io.Discard, "", log.LstdFlags)
	}

	legoLogMu.Lock()
	defer legoLogMu.Unlock()

	oldLogger := legoLogger.Default()
	legoLogger.SetDefault(slog.New(slog.NewTextHandler(logger.Writer(), nil)))
	defer legoLogger.SetDefault(oldLogger)

	return fn()
}

func withLegoLoggerTimeout(logger *log.Logger, timeout time.Duration, fn func(context.Context) error) error {
	return withLegoLogger(logger, func() error {
		ctx, cancel := context.WithTimeout(context.Background(), timeout)
		defer cancel()
		return fn(ctx)
	})
}

func newWebsiteSSLLegoClient(ctx context.Context, acmeAccount *model.WebsiteAcmeAccount) (*ssl.AcmeClient, error) {
	client, err := ssl.NewAcmeClientWithContext(ctx, acmeAccount, getSystemProxy(acmeAccount.UseProxy))
	if err == nil {
		return client, nil
	}
	if !errors.Is(err, ssl.ErrAcmeAccountURLMissing) {
		return nil, err
	}

	client, err = ssl.NewRegisterClientWithContext(ctx, acmeAccount, getSystemProxy(acmeAccount.UseProxy))
	if err != nil {
		return nil, err
	}
	if client.User.GetRegistration() == nil || client.User.GetRegistration().Location == "" {
		return nil, ssl.ErrAcmeAccountURLMissing
	}

	acmeAccount.URL = client.User.GetRegistration().Location
	if acmeAccount.PrivateKey == "" {
		return nil, errors.New("private key can not blank")
	}
	if err := websiteAcmeRepo.Save(*acmeAccount); err != nil {
		return nil, err
	}
	return client, nil
}

type IWebsiteSSLService interface {
	Page(search request.WebsiteSSLSearch) (int64, []response.WebsiteSSLDTO, error)
	GetSSL(id uint) (*response.WebsiteSSLDTO, error)
	Search(req request.WebsiteSSLListReq) ([]response.WebsiteSSLDTO, error)
	Create(create request.WebsiteSSLCreate) (request.WebsiteSSLCreate, error)
	GetDNSResolve(req request.WebsiteDNSReq) ([]response.WebsiteDNSRes, error)
	GetWebsiteSSL(websiteId uint) (response.WebsiteSSLDTO, error)
	Delete(ids []uint) error
	Update(update request.WebsiteSSLUpdate) error
	Upload(req request.WebsiteSSLUpload) error
	PushToNode(req request.WebsiteSSLPush) error
	ObtainSSL(apply request.WebsiteSSLApply) error
	AutoRenewSSL(id uint) error
	SyncForRestart() error
	DownloadFile(id uint) (*os.File, error)
	ImportMasterSSL(create model.WebsiteSSL) error
}

func NewIWebsiteSSLService() IWebsiteSSLService {
	return &WebsiteSSLService{}
}

func (w WebsiteSSLService) Page(search request.WebsiteSSLSearch) (int64, []response.WebsiteSSLDTO, error) {
	var (
		result []response.WebsiteSSLDTO
		opts   []repo.DBOption
	)
	if search.OrderBy != "" && search.Order != "null" {
		opts = append(opts, repo.WithOrderRuleBy(search.OrderBy, search.Order))
	} else {
		opts = append(opts, repo.WithOrderDesc("updated_at"))
	}
	if search.Domain != "" {
		opts = append(opts, websiteSSLRepo.WithByDomain(search.Domain))
	}
	total, sslList, err := websiteSSLRepo.Page(search.Page, search.PageSize, opts...)
	if err != nil {
		return 0, nil, err
	}
	for _, model := range sslList {
		result = append(result, response.WebsiteSSLDTO{
			WebsiteSSL: model,
			LogPath:    path.Join(global.Dir.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", model.PrimaryDomain, model.ID)),
		})
	}
	return total, result, err
}

func (w WebsiteSSLService) GetSSL(id uint) (*response.WebsiteSSLDTO, error) {
	var res response.WebsiteSSLDTO
	websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(id))
	if err != nil {
		return nil, err
	}
	res.WebsiteSSL = *websiteSSL
	return &res, nil
}

func (w WebsiteSSLService) Search(search request.WebsiteSSLListReq) ([]response.WebsiteSSLDTO, error) {
	var (
		opts   []repo.DBOption
		result []response.WebsiteSSLDTO
	)
	opts = append(opts, repo.WithOrderDesc("updated_at"))
	if search.AcmeAccountID != "" {
		acmeAccountID, err := strconv.ParseUint(search.AcmeAccountID, 10, 64)
		if err != nil {
			return nil, err
		}
		opts = append(opts, websiteSSLRepo.WithByAcmeAccountId(uint(acmeAccountID)))
	}
	sslList, err := websiteSSLRepo.List(opts...)
	if err != nil {
		return nil, err
	}
	for _, sslModel := range sslList {
		result = append(result, response.WebsiteSSLDTO{
			WebsiteSSL: sslModel,
		})
	}
	return result, err
}

func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.WebsiteSSLCreate, error) {
	if create.Nameserver1 != "" && !common.IsValidIP(create.Nameserver1) {
		return create, buserr.New("ErrParseIP")
	}
	if create.Nameserver2 != "" && !common.IsValidIP(create.Nameserver2) {
		return create, buserr.New("ErrParseIP")
	}
	var res request.WebsiteSSLCreate
	acmeAccount, err := websiteAcmeRepo.GetFirst(repo.WithByID(create.AcmeAccountID))
	if err != nil {
		return res, err
	}
	websiteSSL := model.WebsiteSSL{
		Status:        constant.SSLInit,
		Provider:      create.Provider,
		AcmeAccountID: acmeAccount.ID,
		PrimaryDomain: create.PrimaryDomain,
		ExpireDate:    time.Now(),
		KeyType:       create.KeyType,
		PushDir:       create.PushDir,
		Description:   create.Description,
		Nameserver1:   create.Nameserver1,
		Nameserver2:   create.Nameserver2,
		SkipDNS:       create.SkipDNS,
		DisableCNAME:  create.DisableCNAME,
		ExecShell:     create.ExecShell,
		IsIp:          create.IsIp,
	}
	if create.ExecShell {
		websiteSSL.Shell = create.Shell
	}
	if create.PushDir {
		fileOP := files.NewFileOp()
		if !fileOP.Stat(create.Dir) {
			return res, buserr.New("ErrLinkPathNotFound")
		}
		websiteSSL.Dir = create.Dir
	}
	setSSLPushConfig(&websiteSSL, create.PushNode, create.Nodes)

	var domains []string
	if create.OtherDomains != "" {
		otherDomainArray := strings.Split(create.OtherDomains, "\n")
		for _, domain := range otherDomainArray {
			if !common.IsValidDomain(domain) {
				err = buserr.WithName("ErrDomainFormat", domain)
				return res, err
			}
			domains = append(domains, domain)
		}
	}
	if create.Provider == constant.Http {
		if strings.Contains(create.PrimaryDomain, "*") {
			return res, buserr.New("ErrWildcardDomain")
		}
		for _, domain := range domains {
			if strings.Contains(domain, "*") {
				return res, buserr.New("ErrWildcardDomain")
			}
		}
	}
	websiteSSL.Domains = strings.Join(domains, ",")

	if create.Provider == constant.DNSAccount || create.Provider == constant.Http {
		websiteSSL.AutoRenew = create.AutoRenew
	}
	if create.Provider == constant.DNSAccount {
		dnsAccount, err := websiteDnsRepo.GetFirst(repo.WithByID(create.DnsAccountID))
		if err != nil {
			return res, err
		}
		websiteSSL.DnsAccountID = dnsAccount.ID
	}

	if err := websiteSSLRepo.Create(context.TODO(), &websiteSSL); err != nil {
		return res, err
	}
	create.ID = websiteSSL.ID
	logFile, err := os.OpenFile(path.Join(global.Dir.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", websiteSSL.PrimaryDomain, websiteSSL.ID)), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.FilePerm)
	if err != nil {
		global.LOG.Errorf("open ssl log file failed, domain: %s, err: %v", websiteSSL.PrimaryDomain, err)
	} else {
		logFile.Close()
	}
	go func() {
		if create.Provider != constant.DnsManual {
			if err = w.ObtainSSL(request.WebsiteSSLApply{
				ID: websiteSSL.ID,
			}); err != nil {
				global.LOG.Errorf("obtain ssl failed, err: %v", err)
			}
		}
	}()
	return create, nil
}

func printSSLLog(logger *log.Logger, msgKey string, params map[string]interface{}) {
	if logger == nil {
		return
	}
	logger.Println(i18n.GetMsgWithMap(msgKey, params))
}

func normalizeSSLPushConfig(pushNode bool, nodes string) (bool, string) {
	nodes = strings.TrimSpace(nodes)
	if !pushNode || nodes == "" {
		return false, ""
	}
	return true, nodes
}

func setSSLPushConfig(websiteSSL *model.WebsiteSSL, pushNode bool, nodes string) {
	pushNode, nodes = normalizeSSLPushConfig(pushNode, nodes)
	if !global.IsMaster || !xpack.MultiNodeProvider.IsXpack() {
		pushNode = false
		nodes = ""
	}
	websiteSSL.PushNode = pushNode
	websiteSSL.Nodes = nodes
}

func pushSSLToNode(websiteSSL *model.WebsiteSSL, logger *log.Logger) error {
	printSSLLog(logger, "StartPushSSLToNode", nil)
	if err := xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil {
		printSSLLog(logger, "PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()})
		return err
	}
	printSSLLog(logger, "PushSSLToNodeSuccess", nil)
	return nil
}

func pushSSLToNodeWithNewLogger(websiteSSL *model.WebsiteSSL) error {
	if !websiteSSL.PushNode {
		return nil
	}
	logFile, logger := newWebsiteSSLLogger(websiteSSL, false)
	if logFile != nil {
		defer func() {
			_ = logFile.Close()
		}()
	}
	return pushSSLToNode(websiteSSL, logger)
}

func newWebsiteSSLLogger(websiteSSL *model.WebsiteSSL, autoRenew bool) (*os.File, *log.Logger) {
	flags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
	if autoRenew {
		flags = os.O_CREATE | os.O_WRONLY | os.O_APPEND
	}

	logFile, err := os.OpenFile(websiteSSL.GetLogPath(), flags, constant.FilePerm)
	if err != nil {
		global.LOG.Errorf("open ssl log file failed, domain: %s, err: %v", websiteSSL.PrimaryDomain, err)
		return nil, log.New(io.Discard, "", log.LstdFlags)
	}

	if autoRenew {
		if info, statErr := logFile.Stat(); statErr == nil && info.Size() > 0 {
			_, _ = logFile.WriteString("\n")
		}
		_, _ = logFile.WriteString(fmt.Sprintf("========== [%s] auto renew attempt ==========\n", time.Now().Format(constant.DateTimeLayout)))
	}

	return logFile, log.New(logFile, "", log.LstdFlags)
}

func reloadSystemSSL(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
	if !global.IsMaster {
		return
	}
	systemSSLEnable, sslID := GetSystemSSL()
	if systemSSLEnable && sslID == websiteSSL.ID {
		fileOp := files.NewFileOp()
		certPath := path.Join(global.Dir.DataDir, "secret/server.crt")
		keyPath := path.Join(global.Dir.DataDir, "secret/server.key")
		printSSLLog(logger, "StartUpdateSystemSSL", nil)
		if err := fileOp.WriteFile(certPath, strings.NewReader(websiteSSL.Pem), 0600); err != nil {
			logger.Printf("Failed to update the SSL certificate File for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error())
			return
		}
		if err := fileOp.WriteFile(keyPath, strings.NewReader(websiteSSL.PrivateKey), 0600); err != nil {
			logger.Printf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error())
			return
		}
		if err := req_helper.PostLocalCore("/core/settings/ssl/reload"); err != nil {
			logger.Printf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error())
			return
		}
		printSSLLog(logger, "UpdateSystemSSLSuccess", nil)
	}
}

// SyncSystemSSL reconciles the panel certificate on disk with the WebsiteSSL
// row referenced by the SSLID setting. When they differ (typically because a
// previous renewal's reloadSystemSSL was skipped by a transient nginx reload
// failure or because the panel was restarted between the DB save and the file
// rewrite), the on-disk cert is refreshed and core is asked to reload its TLS
// store. Safe to call on every renewal cron tick: it is a no-op when SSL is
// not enabled, when SSLID does not resolve, or when the cert already matches.
func SyncSystemSSL() {
	if !global.IsMaster {
		return
	}
	systemSSLEnable, sslID := GetSystemSSL()
	if !systemSSLEnable {
		return
	}
	websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(sslID))
	if err != nil {
		return
	}
	if strings.TrimSpace(websiteSSL.Pem) == "" || strings.TrimSpace(websiteSSL.PrivateKey) == "" {
		return
	}
	certPath := path.Join(global.Dir.DataDir, "secret/server.crt")
	keyPath := path.Join(global.Dir.DataDir, "secret/server.key")
	diskCert, _ := os.ReadFile(certPath)
	diskKey, _ := os.ReadFile(keyPath)
	if strings.TrimSpace(string(diskCert)) == strings.TrimSpace(websiteSSL.Pem) &&
		strings.TrimSpace(string(diskKey)) == strings.TrimSpace(websiteSSL.PrivateKey) {
		return
	}
	global.LOG.Infof("panel SSL on disk diverged from DB (SSLID=%d, domain=%s), syncing", websiteSSL.ID, websiteSSL.PrimaryDomain)
	fileOp := files.NewFileOp()
	if err := fileOp.WriteFile(certPath, strings.NewReader(websiteSSL.Pem), 0600); err != nil {
		global.LOG.Errorf("sync panel SSL: write cert failed: %s", err.Error())
		return
	}
	if err := fileOp.WriteFile(keyPath, strings.NewReader(websiteSSL.PrivateKey), 0600); err != nil {
		global.LOG.Errorf("sync panel SSL: write key failed: %s", err.Error())
		return
	}
	if err := req_helper.PostLocalCore("/core/settings/ssl/reload"); err != nil {
		global.LOG.Errorf("sync panel SSL: notify core failed: %s", err.Error())
		return
	}
	global.LOG.Info("panel SSL synced from DB to disk")
}

func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
	return w.obtainSSL(apply.ID, false)
}

func (w WebsiteSSLService) AutoRenewSSL(id uint) error {
	return w.obtainSSL(id, true)
}

func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
	var (
		err         error
		websiteSSL  *model.WebsiteSSL
		acmeAccount *model.WebsiteAcmeAccount
		dnsAccount  *model.WebsiteDnsAccount
		logFile     *os.File
		logger      *log.Logger
		resource    certificate.Resource
		httpRoot    string
	)

	websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(id))
	if err != nil {
		return err
	}
	if websiteSSL.Status == constant.SSLApply {
		return buserr.New("InExecuting")
	}
	acmeAccount, err = websiteAcmeRepo.GetFirst(repo.WithByID(websiteSSL.AcmeAccountID))
	if err != nil {
		return err
	}
	domains := []string{websiteSSL.PrimaryDomain}
	if websiteSSL.Domains != "" {
		domains = append(domains, strings.Split(websiteSSL.Domains, ",")...)
	}
	if websiteSSL.Provider != constant.DnsManual {
		switch websiteSSL.Provider {
		case constant.DNSAccount:
			dnsAccount, err = websiteDnsRepo.GetFirst(repo.WithByID(websiteSSL.DnsAccountID))
			if err != nil {
				return err
			}
		case constant.Http:
			appInstall, err := getAppInstallByKey(constant.AppOpenresty)
			if err != nil {
				if errors.Is(err, gormv2.ErrRecordNotFound) {
					return buserr.New("ErrOpenrestyNotFound")
				}
				return err
			}
			for _, domain := range domains {
				if strings.Contains(domain, "*") {
					return buserr.New("ErrWildcardDomain")
				}
			}
			httpRoot = path.Join(appInstall.GetPath(), "root")
		}
	}
	marked, err := websiteSSLRepo.TryMarkApplying(websiteSSL.ID)
	if err != nil {
		return err
	}
	if !marked {
		return buserr.New("InExecuting")
	}
	websiteSSL.Status = constant.SSLApply

	logFile, logger = newWebsiteSSLLogger(websiteSSL, autoRenew)
	logFileOwnedByGoroutine := false
	if logFile != nil {
		defer func() {
			if !logFileOwnedByGoroutine {
				_ = logFile.Close()
			}
		}()
	}
	startMsg := i18n.GetMsgWithMap("ApplySSLStart", map[string]interface{}{"domain": strings.Join(domains, ","), "type": i18n.GetMsgByKey(websiteSSL.Provider)})
	if websiteSSL.Provider == constant.DNSAccount {
		startMsg = startMsg + i18n.GetMsgWithMap("DNSAccountName", map[string]interface{}{"name": dnsAccount.Name, "type": dnsAccount.Type})
	}
	logger.Println(startMsg)

	logFileOwnedByGoroutine = true
	go func(logFile *os.File, logger *log.Logger) {
		if logFile != nil {
			defer func() {
				_ = logFile.Close()
			}()
		}
		if websiteSSL.Provider != constant.DnsManual {
			privateKey, err := ssl.GetPrivateKeyByType(websiteSSL.KeyType, websiteSSL.PrivateKey)
			if err != nil {
				handleError(websiteSSL, logger, err)
				return
			}
			err = withLegoLoggerTimeout(logger, sslObtainTimeout, func(ctx context.Context) error {
				client, err := newWebsiteSSLLegoClient(ctx, acmeAccount)
				if err != nil {
					return err
				}
				switch websiteSSL.Provider {
				case constant.DNSAccount:
					if err = client.UseDns(ssl.DnsType(dnsAccount.Type), dnsAccount.Authorization, *websiteSSL); err != nil {
						return err
					}
				case constant.Http:
					if err = client.UseHTTP(httpRoot); err != nil {
						return err
					}
				}
				if websiteSSL.IsIp {
					resource, err = client.ObtainIPSSL(ctx, domains[0], privateKey)
				} else {
					resource, err = client.ObtainSSL(ctx, domains, privateKey)
				}
				return err
			})
			if err != nil {
				handleError(websiteSSL, logger, err)
				return
			}
		} else {
			manualClient, err := ssl.NewCustomAcmeClient(acmeAccount, getSystemProxy(acmeAccount.UseProxy), logger)
			if err != nil {
				handleError(websiteSSL, logger, err)
				return
			}
			ctx, cancel := context.WithTimeout(context.Background(), sslObtainTimeout)
			resource, err = manualClient.RequestCertificate(ctx, websiteSSL)
			cancel()
			if err != nil {
				handleError(websiteSSL, logger, err)
				return
			}
		}

		websiteSSL.PrivateKey = string(resource.PrivateKey)
		websiteSSL.Pem = string(resource.Certificate)
		websiteSSL.CertURL = resource.CertURL
		cert, err := parseCertificatePEM(resource.Certificate)
		if err != nil {
			handleError(websiteSSL, logger, err)
			return
		}
		websiteSSL.ExpireDate = cert.NotAfter
		websiteSSL.StartDate = cert.NotBefore
		websiteSSL.Type = cert.Issuer.CommonName
		if len(cert.Issuer.Organization) > 0 {
			websiteSSL.Organization = cert.Issuer.Organization[0]
		}
		websiteSSL.Status = constant.SSLReady
		printSSLLog(logger, "ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")})
		saveCertificateFile(websiteSSL, logger)

		if websiteSSL.ExecShell {
			workDir := global.Dir.DataDir
			if websiteSSL.PushDir {
				workDir = websiteSSL.Dir
			}
			printSSLLog(logger, "ExecShellStart", nil)
			if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil {
				printSSLLog(logger, "ErrExecShell", map[string]interface{}{"err": err.Error()})
			} else {
				printSSLLog(logger, "ExecShellSuccess", nil)
			}
		}

		err = websiteSSLRepo.Save(websiteSSL)
		if err != nil {
			return
		}

		websites, _ := websiteRepo.GetBy(websiteRepo.WithWebsiteSSLID(websiteSSL.ID))
		if len(websites) > 0 {
			for _, website := range websites {
				printSSLLog(logger, "ApplyWebSiteSSLLog", map[string]interface{}{"name": website.PrimaryDomain})
				if err := createPemFile(website, *websiteSSL); err != nil {
					printSSLLog(logger, "ErrUpdateWebsiteSSL", map[string]interface{}{"name": website.PrimaryDomain, "err": err.Error()})
				}
			}
			if nginxInstall, err := getAppInstallByKey(constant.AppOpenresty); err == nil {
				if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
					printSSLLog(logger, "ErrSSLApply", nil)
				} else {
					printSSLLog(logger, "ApplyWebSiteSSLSuccess", nil)
				}
			}
		}
		reloadSystemSSL(websiteSSL, logger)
		if websiteSSL.PushNode {
			if err = pushSSLToNode(websiteSSL, logger); err != nil {
				return
			}
		}
	}(logFile, logger)

	return nil
}

func runShellScriptFile(workDir, shell string, logger *log.Logger) error {
	file, err := os.CreateTemp("", "1panel-shell-*.sh")
	if err != nil {
		return err
	}
	defer func() { _ = os.Remove(file.Name()) }()
	if _, err := file.WriteString(shell); err != nil {
		_ = file.Close()
		return err
	}
	if err := file.Close(); err != nil {
		return err
	}
	cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(30*time.Minute), cmd.WithLogger(logger), cmd.WithWorkDir(workDir))
	return cmdMgr.Run("bash", file.Name())
}

func parseCertificatePEM(certPEM []byte) (*x509.Certificate, error) {
	certBlock, _ := pem.Decode(certPEM)
	if certBlock == nil {
		return nil, errors.New("invalid certificate PEM")
	}
	return x509.ParseCertificate(certBlock.Bytes)
}

func handleError(websiteSSL *model.WebsiteSSL, logger *log.Logger, err error) {
	if websiteSSL.Status == constant.SSLInit || websiteSSL.Status == constant.SSLError {
		websiteSSL.Status = constant.StatusError
	} else {
		websiteSSL.Status = constant.SSLApplyError
	}
	websiteSSL.Message = err.Error()
	if logger != nil {
		logger.Println(i18n.GetErrMsg("ApplySSLFailed", map[string]interface{}{"domain": websiteSSL.PrimaryDomain, "detail": err.Error()}))
	}
	_ = websiteSSLRepo.Save(websiteSSL)
}

func (w WebsiteSSLService) GetDNSResolve(req request.WebsiteDNSReq) ([]response.WebsiteDNSRes, error) {
	acmeAccount, err := websiteAcmeRepo.GetFirst(repo.WithByID(req.AcmeAccountID))
	if err != nil {
		return nil, err
	}
	client, err := ssl.NewCustomAcmeClient(acmeAccount, getSystemProxy(acmeAccount.UseProxy), nil)
	if err != nil {
		return nil, err
	}
	websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(req.WebsiteSSLID))
	if err != nil {
		return nil, err
	}
	resolves, err := client.GetDNSResolve(context.TODO(), websiteSSL)
	if err != nil {
		return nil, err
	}
	var res []response.WebsiteDNSRes
	for k, v := range resolves {
		res = append(res, response.WebsiteDNSRes{
			Domain: k,
			Key:    v.Key,
			Value:  v.Value,
			Err:    v.Err,
		})
	}
	return res, nil
}

func (w WebsiteSSLService) GetWebsiteSSL(websiteId uint) (response.WebsiteSSLDTO, error) {
	var res response.WebsiteSSLDTO
	website, err := websiteRepo.GetFirst(repo.WithByID(websiteId))
	if err != nil {
		return res, err
	}
	websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(website.WebsiteSSLID))
	if err != nil {
		return res, err
	}
	res.WebsiteSSL = *websiteSSL
	return res, nil
}

func (w WebsiteSSLService) Delete(ids []uint) error {
	var (
		websiteSSLS []string
		applySSLS   []string
	)
	for _, id := range ids {
		if websites, _ := websiteRepo.GetBy(websiteRepo.WithWebsiteSSLID(id)); len(websites) > 0 {
			oldSSL, _ := websiteSSLRepo.GetFirst(repo.WithByID(id))
			if oldSSL != nil && oldSSL.ID > 0 {
				websiteSSLS = append(websiteSSLS, oldSSL.PrimaryDomain)
			}
			continue
		}
		sslSetting, _ := settingRepo.Get(settingRepo.WithByKey("SSL"))
		if sslSetting.Value == "enable" {
			sslID, _ := settingRepo.Get(settingRepo.WithByKey("SSLID"))
			idValue, _ := strconv.Atoi(sslID.Value)
			if idValue > 0 && uint(idValue) == id {
				return buserr.New("ErrDeleteWithPanelSSL")
			}
		}
		websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(id))
		if err != nil {
			return err
		}
		if websiteSSL.Status == constant.SSLApply {
			applySSLS = append(applySSLS, websiteSSL.PrimaryDomain)
			continue
		}
		if websiteSSL.Provider != constant.Manual && websiteSSL.Provider != constant.SelfSigned {
			go func() {
				acmeAccount, err := websiteAcmeRepo.GetFirst(repo.WithByID(websiteSSL.AcmeAccountID))
				if err != nil {
					global.LOG.Errorf("Failed to get acme account for SSL revoke, err: %v", err)
					return
				}
				err = withLegoLogger(nil, func() error {
					client, err := newWebsiteSSLLegoClient(context.Background(), acmeAccount)
					if err != nil {
						return err
					}
					return client.RevokeSSL([]byte(websiteSSL.Pem))
				})
				if err != nil {
					global.LOG.Errorf("Failed to revoke SSL for domain %s, err: %v", websiteSSL.PrimaryDomain, err)
					return
				}
			}()
		}
		_ = websiteSSLRepo.DeleteBy(repo.WithByID(id))
	}
	if len(websiteSSLS) > 0 {
		return buserr.WithName("ErrSSLCannotDelete", strings.Join(websiteSSLS, ","))
	}
	if len(applySSLS) > 0 {
		return buserr.WithName("ErrApplySSLCanNotDelete", strings.Join(applySSLS, ","))
	}
	return nil
}

func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error {
	websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(update.ID))
	if err != nil {
		return err
	}
	updateParams := make(map[string]interface{})
	updateParams["primary_domain"] = update.PrimaryDomain
	updateParams["description"] = update.Description
	updateParams["provider"] = update.Provider
	updateParams["push_dir"] = update.PushDir
	updateParams["disable_cname"] = update.DisableCNAME
	updateParams["skip_dns"] = update.SkipDNS
	updateParams["nameserver1"] = update.Nameserver1
	updateParams["nameserver2"] = update.Nameserver2
	updateParams["exec_shell"] = update.ExecShell
	if update.ExecShell {
		updateParams["shell"] = update.Shell
	} else {
		updateParams["shell"] = ""
	}
	pushNode, nodes := normalizeSSLPushConfig(update.PushNode, update.Nodes)
	if !global.IsMaster || !xpack.MultiNodeProvider.IsXpack() {
		pushNode = false
		nodes = ""
	}
	updateParams["push_node"] = pushNode
	updateParams["nodes"] = nodes

	if websiteSSL.Provider != constant.SelfSigned && websiteSSL.Provider != constant.Manual {
		acmeAccount, err := websiteAcmeRepo.GetFirst(repo.WithByID(update.AcmeAccountID))
		if err != nil {
			return err
		}
		updateParams["acme_account_id"] = acmeAccount.ID
	}

	if update.PushDir {
		fileOP := files.NewFileOp()
		if !fileOP.Stat(update.Dir) {
			_ = fileOP.CreateDir(update.Dir, constant.DirPerm)
		}
		updateParams["dir"] = update.Dir
	}
	var domains []string
	if update.OtherDomains != "" {
		otherDomainArray := strings.Split(update.OtherDomains, "\n")
		for _, domain := range otherDomainArray {
			if websiteSSL.Provider != constant.SelfSigned && !common.IsValidDomain(domain) {
				return buserr.WithName("ErrDomainFormat", domain)
			}
			domains = append(domains, domain)
		}
	}
	updateParams["domains"] = strings.Join(domains, ",")
	if update.Provider == constant.DNSAccount || update.Provider == constant.Http || update.Provider == constant.SelfSigned {
		updateParams["auto_renew"] = update.AutoRenew
	} else {
		updateParams["auto_renew"] = false
	}
	if update.Provider == constant.DNSAccount {
		dnsAccount, err := websiteDnsRepo.GetFirst(repo.WithByID(update.DnsAccountID))
		if err != nil {
			return err
		}
		updateParams["dns_account_id"] = dnsAccount.ID
	} else {
		updateParams["dns_account_id"] = 0
	}
	return websiteSSLRepo.SaveByMap(websiteSSL, updateParams)
}

func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error {
	websiteSSL := &model.WebsiteSSL{
		Provider:    constant.Manual,
		Description: req.Description,
		Status:      constant.SSLReady,
	}
	setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes)
	var err error
	if req.SSLID > 0 {
		websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID))
		if err != nil {
			return err
		}
		websiteSSL.Description = req.Description
		setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes)
	}
	if req.Type == "local" {
		fileOp := files.NewFileOp()
		if !fileOp.Stat(req.PrivateKeyPath) {
			return buserr.New("ErrSSLKeyNotFound")
		}
		if !fileOp.Stat(req.CertificatePath) {
			return buserr.New("ErrSSLCertificateNotFound")
		}
		if content, err := fileOp.GetContent(req.PrivateKeyPath); err != nil {
			return err
		} else {
			websiteSSL.PrivateKey = string(content)
		}
		if content, err := fileOp.GetContent(req.CertificatePath); err != nil {
			return err
		} else {
			websiteSSL.Pem = string(content)
		}
		websiteSSL.CertPath = req.CertificatePath
		websiteSSL.PrivateKeyPath = req.PrivateKeyPath
	} else {
		websiteSSL.PrivateKey = req.PrivateKey
		websiteSSL.Pem = req.Certificate
		websiteSSL.CertPath = ""
		websiteSSL.PrivateKeyPath = ""
	}

	privateKeyCertBlock, _ := pem.Decode([]byte(websiteSSL.PrivateKey))
	if privateKeyCertBlock == nil {
		return buserr.New("ErrSSLKeyFormat")
	}

	var (
		cert    *x509.Certificate
		pemData = []byte(websiteSSL.Pem)
	)
	for {
		certBlock, reset := pem.Decode(pemData)
		if certBlock == nil {
			break
		}
		cert, err = x509.ParseCertificate(certBlock.Bytes)
		if err != nil {
			return err
		}
		if len(cert.DNSNames) > 0 || len(cert.IPAddresses) > 0 {
			break
		}
		pemData = reset
	}
	if pemData == nil || cert == nil {
		return buserr.New("ErrSSLCertificateFormat")
	}

	websiteSSL.ExpireDate = cert.NotAfter
	websiteSSL.StartDate = cert.NotBefore
	websiteSSL.Type = cert.Issuer.CommonName
	if len(cert.Issuer.Organization) > 0 {
		websiteSSL.Organization = cert.Issuer.Organization[0]
	} else {
		websiteSSL.Organization = cert.Issuer.CommonName
	}

	var domains []string
	if len(cert.DNSNames) > 0 {
		websiteSSL.PrimaryDomain = cert.DNSNames[0]
		domains = cert.DNSNames[1:]
	}
	if len(cert.IPAddresses) > 0 {
		if websiteSSL.PrimaryDomain == "" {
			websiteSSL.PrimaryDomain = cert.IPAddresses[0].String()
			for _, ip := range cert.IPAddresses[1:] {
				domains = append(domains, ip.String())
			}
		} else {
			for _, ip := range cert.IPAddresses {
				domains = append(domains, ip.String())
			}
		}
	}
	websiteSSL.Domains = strings.Join(domains, ",")

	if websiteSSL.ID > 0 {
		if err := UpdateSSLConfig(*websiteSSL); err != nil {
			return err
		}
		if err := websiteSSLRepo.Save(websiteSSL); err != nil {
			return err
		}
		return pushSSLToNodeWithNewLogger(websiteSSL)
	}
	if err := websiteSSLRepo.Create(context.Background(), websiteSSL); err != nil {
		return err
	}
	return pushSSLToNodeWithNewLogger(websiteSSL)
}

func (w WebsiteSSLService) PushToNode(req request.WebsiteSSLPush) error {
	if !global.IsMaster {
		return errors.New("only master node can push SSL to nodes")
	}
	if !xpack.MultiNodeProvider.IsXpack() {
		return errors.New("SSL node push is an XPack feature")
	}
	pushNode, nodes := normalizeSSLPushConfig(req.PushNode, req.Nodes)
	if !pushNode {
		return errors.New("please select nodes to push SSL")
	}
	websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(req.ID))
	if err != nil {
		return err
	}
	if websiteSSL.Provider == constant.FromMaster {
		return errors.New("SSL imported from master node can not be pushed")
	}
	if websiteSSL.Status != constant.SSLReady {
		return errors.New("only ready SSL can be pushed")
	}
	if task.CheckResourceTaskIsExecuting(task.TaskPush, task.TaskScopeWebsite, websiteSSL.ID) {
		return buserr.New("TaskIsExecuting")
	}
	if err := websiteSSLRepo.SaveByMap(websiteSSL, map[string]interface{}{
		"push_node": pushNode,
		"nodes":     nodes,
	}); err != nil {
		return err
	}
	websiteSSL.PushNode = pushNode
	websiteSSL.Nodes = nodes

	if req.Sync {
		return xpack.MultiNodeProvider.PushSSLToNode(websiteSSL)
	}

	pushTask, err := task.NewTask(task.GetTaskName(websiteSSL.PrimaryDomain, task.TaskPush, "SSL"), task.TaskPush, task.TaskScopeWebsite, req.TaskID, websiteSSL.ID)
	if err != nil {
		return err
	}
	pushTask.AddSubTask(i18n.GetMsgByKey("StartPushSSLToNode"), func(t *task.Task) error {
		t.Log(i18n.GetMsgByKey("StartPushSSLToNode"))
		if err := xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil {
			t.Log(i18n.GetMsgWithMap("PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()}))
			return err
		}
		t.Log(i18n.GetMsgByKey("PushSSLToNodeSuccess"))
		return nil
	}, nil)
	go func() {
		if err := pushTask.Execute(); err != nil {
			global.LOG.Errorf("push ssl to node failed, sslID: %d, err: %v", websiteSSL.ID, err)
		}
	}()
	return nil
}

func (w WebsiteSSLService) DownloadFile(id uint) (*os.File, error) {
	websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(id))
	if err != nil {
		return nil, err
	}
	fileOp := files.NewFileOp()
	dir := path.Join(global.Dir.DataDir, "tmp/ssl", websiteSSL.PrimaryDomain)
	if fileOp.Stat(dir) {
		if err = fileOp.DeleteDir(dir); err != nil {
			return nil, err
		}
	}
	if err = fileOp.CreateDir(dir, constant.DirPerm); err != nil {
		return nil, err
	}
	if err = fileOp.WriteFile(path.Join(dir, "fullchain.pem"), strings.NewReader(websiteSSL.Pem), constant.DirPerm); err != nil {
		return nil, err
	}
	if err = fileOp.WriteFile(path.Join(dir, "privkey.pem"), strings.NewReader(websiteSSL.PrivateKey), constant.DirPerm); err != nil {
		return nil, err
	}
	fileName := websiteSSL.PrimaryDomain + ".zip"
	if err = fileOp.Compress(context.Background(), []string{path.Join(dir, "fullchain.pem"), path.Join(dir, "privkey.pem")}, dir, fileName, files.SdkZip, "", nil); err != nil {
		return nil, err
	}
	return os.Open(path.Join(dir, fileName))
}

func (w WebsiteSSLService) SyncForRestart() error {
	sslList, err := websiteSSLRepo.List()
	if err != nil {
		return err
	}
	for _, ssl := range sslList {
		if ssl.Status == constant.SSLApply {
			ssl.Status = constant.SystemRestart
			ssl.Message = "System restart causing interrupt"
			_ = websiteSSLRepo.Save(&ssl)
		}
	}
	return nil
}

func (w WebsiteSSLService) ImportMasterSSL(create model.WebsiteSSL) error {
	websiteSSL, err := websiteSSLRepo.GetFirst(websiteSSLRepo.WithByMasterSSLID(create.ID))
	if err != nil {
		if !errors.Is(err, gormv2.ErrRecordNotFound) {
			return err
		}
		websiteSSL = &model.WebsiteSSL{}
	}
	websiteSSL.Status = constant.SSLReady
	websiteSSL.Provider = constant.FromMaster
	websiteSSL.PrimaryDomain = create.PrimaryDomain
	websiteSSL.StartDate = create.StartDate
	websiteSSL.ExpireDate = create.ExpireDate
	websiteSSL.KeyType = create.KeyType
	websiteSSL.Description = create.Description
	websiteSSL.PrivateKey = create.PrivateKey
	websiteSSL.Pem = create.Pem
	websiteSSL.Type = create.Type
	websiteSSL.Organization = create.Organization
	websiteSSL.MasterSSLID = create.ID
	websiteSSL.Domains = create.Domains
	if websiteSSL.ID == 0 {
		if err := websiteSSLRepo.Create(context.Background(), websiteSSL); err != nil {
			return err
		}
	} else {
		if err := websiteSSLRepo.Save(websiteSSL); err != nil {
			return err
		}
	}
	websites, _ := websiteRepo.GetBy(websiteRepo.WithWebsiteSSLID(websiteSSL.ID))
	if len(websites) == 0 {
		return nil
	}
	for _, website := range websites {
		if err := createPemFile(website, *websiteSSL); err != nil {
			continue
		}
	}
	nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
	if err == nil {
		if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
			return err
		}
	}
	return nil
}
