package service

import (
	"bytes"
	"context"
	"crypto/ecdsa"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"fmt"
	"log"
	"math/big"
	"net"
	"os"
	"path"
	"strings"
	"time"

	"github.com/1Panel-dev/1Panel/agent/app/repo"
	"github.com/1Panel-dev/1Panel/agent/utils/common"

	"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/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/files"
	"github.com/1Panel-dev/1Panel/agent/utils/ssl"
	"github.com/go-acme/lego/v5/certcrypto"
)

type WebsiteCAService struct {
}

type IWebsiteCAService interface {
	Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error)
	Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error)
	GetCA(id uint) (*response.WebsiteCADTO, error)
	Delete(id uint) error
	ObtainSSL(req request.WebsiteCAObtain) (*model.WebsiteSSL, error)
	DownloadFile(id uint) (*os.File, error)
}

func NewIWebsiteCAService() IWebsiteCAService {
	return &WebsiteCAService{}
}

func (w WebsiteCAService) Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error) {
	total, cas, err := websiteCARepo.Page(search.Page, search.PageSize, repo.WithOrderDesc("created_at"))
	if err != nil {
		return 0, nil, err
	}
	var caDTOs []response.WebsiteCADTO
	for _, ca := range cas {
		caDTOs = append(caDTOs, response.WebsiteCADTO{
			WebsiteCA: ca,
		})
	}
	return total, caDTOs, err
}

func (w WebsiteCAService) Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error) {
	if exist, _ := websiteCARepo.GetFirst(repo.WithByName(create.Name)); exist.ID > 0 {
		return nil, buserr.New("ErrNameIsExist")
	}

	ca := &model.WebsiteCA{
		Name:    create.Name,
		KeyType: create.KeyType,
	}

	pkixName := pkix.Name{
		CommonName:         create.CommonName,
		Country:            []string{create.Country},
		Organization:       []string{create.Organization},
		OrganizationalUnit: []string{create.OrganizationUint},
	}
	if create.Province != "" {
		pkixName.Province = []string{create.Province}
	}
	if create.City != "" {
		pkixName.Locality = []string{create.City}
	}

	rootCA := &x509.Certificate{
		SerialNumber:          big.NewInt(time.Now().Unix() + 1),
		Subject:               pkixName,
		NotBefore:             time.Now(),
		NotAfter:              time.Now().AddDate(10, 0, 0),
		BasicConstraintsValid: true,
		IsCA:                  true,
		MaxPathLen:            1,
		MaxPathLenZero:        false,
		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
	}

	interPrivateKey, interPublicKey, privateBytes, err := createPrivateKey(create.KeyType)
	if err != nil {
		return nil, err
	}
	ca.PrivateKey = string(privateBytes)

	rootDer, err := x509.CreateCertificate(rand.Reader, rootCA, rootCA, interPublicKey, interPrivateKey)
	if err != nil {
		return nil, err
	}
	rootCert, err := x509.ParseCertificate(rootDer)
	if err != nil {
		return nil, err
	}
	certBlock := &pem.Block{
		Type:  "CERTIFICATE",
		Bytes: rootCert.Raw,
	}
	pemData := pem.EncodeToMemory(certBlock)
	ca.CSR = string(pemData)

	if err := websiteCARepo.Create(context.Background(), ca); err != nil {
		return nil, err
	}
	return &create, nil
}

func (w WebsiteCAService) GetCA(id uint) (*response.WebsiteCADTO, error) {
	res := &response.WebsiteCADTO{}
	ca, err := websiteCARepo.GetFirst(repo.WithByID(id))
	if err != nil {
		return nil, err
	}
	res.WebsiteCA = ca
	certBlock, _ := pem.Decode([]byte(ca.CSR))
	if certBlock == nil {
		return nil, buserr.New("ErrSSLCertificateFormat")
	}
	cert, err := x509.ParseCertificate(certBlock.Bytes)
	if err != nil {
		return nil, err
	}
	res.CommonName = cert.Issuer.CommonName
	res.Organization = strings.Join(cert.Issuer.Organization, ",")
	res.Country = strings.Join(cert.Issuer.Country, ",")
	res.Province = strings.Join(cert.Issuer.Province, ",")
	res.City = strings.Join(cert.Issuer.Locality, ",")
	res.OrganizationUint = strings.Join(cert.Issuer.OrganizationalUnit, ",")

	return res, nil
}

func (w WebsiteCAService) Delete(id uint) error {
	ssls, _ := websiteSSLRepo.List(websiteSSLRepo.WithByCAID(id))
	if len(ssls) > 0 {
		return buserr.New("ErrDeleteCAWithSSL")
	}
	exist, err := websiteCARepo.GetFirst(repo.WithByID(id))
	if err != nil {
		return err
	}
	if exist.Name == "1Panel" {
		return buserr.New("ErrDefaultCA")
	}
	return websiteCARepo.DeleteBy(repo.WithByID(id))
}

func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.WebsiteSSL, error) {
	var (
		domains    []string
		ips        []net.IP
		websiteSSL = &model.WebsiteSSL{}
		err        error
		ca         model.WebsiteCA
	)
	if req.Renew {
		websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID))
		if err != nil {
			return nil, err
		}
		ca, err = websiteCARepo.GetFirst(repo.WithByID(websiteSSL.CaID))
		if err != nil {
			return nil, err
		}
		existDomains := []string{websiteSSL.PrimaryDomain}
		if websiteSSL.Domains != "" {
			existDomains = append(existDomains, strings.Split(websiteSSL.Domains, ",")...)
		}
		for _, domain := range existDomains {
			if ipAddress := common.ParseIPLoose(domain); ipAddress == nil {
				domains = append(domains, domain)
			} else {
				ips = append(ips, ipAddress)
			}
		}
	} else {
		ca, err = websiteCARepo.GetFirst(repo.WithByID(req.ID))
		if err != nil {
			return nil, err
		}
		websiteSSL = &model.WebsiteSSL{
			Provider:    constant.SelfSigned,
			KeyType:     req.KeyType,
			PushDir:     req.PushDir,
			CaID:        ca.ID,
			AutoRenew:   req.AutoRenew,
			Description: req.Description,
			ExecShell:   req.ExecShell,
		}
		setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes)
		if req.ExecShell {
			websiteSSL.Shell = req.Shell
		}
		if req.PushDir {
			if !files.NewFileOp().Stat(req.Dir) {
				return nil, buserr.New("ErrLinkPathNotFound")
			}
			websiteSSL.Dir = req.Dir
		}
		if req.Domains != "" {
			domainArray := strings.Split(req.Domains, "\n")
			for _, domain := range domainArray {
				if ipAddress := common.ParseIPLoose(domain); ipAddress == nil {
					if domain != "localhost" && !common.IsValidDomain(domain) {
						err = buserr.WithName("ErrDomainFormat", domain)
						return nil, err
					}
					domains = append(domains, domain)
				} else {
					ips = append(ips, ipAddress)
				}
			}
			if len(domains) > 0 {
				websiteSSL.PrimaryDomain = domains[0]
				websiteSSL.Domains = strings.Join(domains[1:], ",")
			}
			ipStrings := make([]string, len(ips))
			for i, ip := range ips {
				ipStrings[i] = ip.String()
			}
			if websiteSSL.PrimaryDomain == "" && len(ips) > 0 {
				websiteSSL.PrimaryDomain = ipStrings[0]
				ipStrings = ipStrings[1:]
			}
			if len(ipStrings) > 0 {
				if websiteSSL.Domains != "" {
					websiteSSL.Domains += ","
				}
				websiteSSL.Domains += strings.Join(ipStrings, ",")
			}

		}
	}

	rootCertBlock, _ := pem.Decode([]byte(ca.CSR))
	if rootCertBlock == nil {
		return nil, buserr.New("ErrSSLCertificateFormat")
	}
	rootCsr, err := x509.ParseCertificate(rootCertBlock.Bytes)
	if err != nil {
		return nil, err
	}
	rootPrivateKeyBlock, _ := pem.Decode([]byte(ca.PrivateKey))
	if rootPrivateKeyBlock == nil {
		return nil, buserr.New("ErrSSLCertificateFormat")
	}

	var rootPrivateKey any
	if ssl.KeyType(ca.KeyType) == certcrypto.EC256 || ssl.KeyType(ca.KeyType) == certcrypto.EC384 {
		rootPrivateKey, err = x509.ParseECPrivateKey(rootPrivateKeyBlock.Bytes)
		if err != nil {
			return nil, err
		}
	} else {
		rootPrivateKey, err = x509.ParsePKCS1PrivateKey(rootPrivateKeyBlock.Bytes)
		if err != nil {
			return nil, err
		}
	}
	interPrivateKey, interPublicKey, _, err := createPrivateKey(websiteSSL.KeyType)
	if err != nil {
		return nil, err
	}
	notAfter := time.Now()
	if req.Unit == "year" {
		notAfter = notAfter.AddDate(req.Time, 0, 0)
	} else {
		notAfter = notAfter.AddDate(0, 0, req.Time)
	}
	interCsr := &x509.Certificate{
		SerialNumber:          big.NewInt(time.Now().Unix() + 2),
		Subject:               rootCsr.Subject,
		NotBefore:             time.Now(),
		NotAfter:              notAfter,
		BasicConstraintsValid: true,
		IsCA:                  true,
		MaxPathLen:            0,
		MaxPathLenZero:        true,
		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
	}
	interDer, err := x509.CreateCertificate(rand.Reader, interCsr, rootCsr, interPublicKey, rootPrivateKey)
	if err != nil {
		return nil, err
	}
	interCert, err := x509.ParseCertificate(interDer)
	if err != nil {
		return nil, err
	}
	interCertBlock := &pem.Block{
		Type:  "CERTIFICATE",
		Bytes: interCert.Raw,
	}
	_, publicKey, privateKeyBytes, err := createPrivateKey(websiteSSL.KeyType)
	if err != nil {
		return nil, err
	}
	commonName := ""
	if len(domains) > 0 {
		commonName = domains[0]
	}
	if len(ips) > 0 {
		commonName = ips[0].String()
	}
	subject := rootCsr.Subject
	subject.CommonName = commonName
	csr := &x509.Certificate{
		SerialNumber:          big.NewInt(time.Now().Unix() + 3),
		Subject:               subject,
		NotBefore:             time.Now(),
		NotAfter:              notAfter,
		BasicConstraintsValid: true,
		IsCA:                  false,
		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		DNSNames:              domains,
		IPAddresses:           ips,
	}

	der, err := x509.CreateCertificate(rand.Reader, csr, interCert, publicKey, interPrivateKey)
	if err != nil {
		return nil, err
	}
	cert, err := x509.ParseCertificate(der)
	if err != nil {
		return nil, err
	}

	certBlock := &pem.Block{
		Type:  "CERTIFICATE",
		Bytes: cert.Raw,
	}
	websiteSSL.Pem = string(pem.EncodeToMemory(certBlock)) + string(pem.EncodeToMemory(rootCertBlock)) + string(pem.EncodeToMemory(interCertBlock))
	websiteSSL.PrivateKey = string(privateKeyBytes)
	websiteSSL.ExpireDate = cert.NotAfter
	websiteSSL.StartDate = cert.NotBefore
	websiteSSL.Type = cert.Issuer.CommonName
	websiteSSL.Organization = rootCsr.Subject.Organization[0]
	websiteSSL.Status = constant.SSLReady

	if req.Renew {
		if err := websiteSSLRepo.Save(websiteSSL); err != nil {
			return nil, err
		}
	} else {
		if err := websiteSSLRepo.Create(context.Background(), websiteSSL); err != nil {
			return nil, err
		}
	}

	logFile, _ := 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)
	defer logFile.Close()
	logger := log.New(logFile, "", log.LstdFlags)
	logger.Println(i18n.GetMsgWithMap("ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")}))
	saveCertificateFile(websiteSSL, logger)
	if websiteSSL.ExecShell {
		workDir := global.Dir.DataDir
		if websiteSSL.PushDir {
			workDir = websiteSSL.Dir
		}
		logger.Println(i18n.GetMsgByKey("ExecShellStart"))
		if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil {
			logger.Println(i18n.GetMsgWithMap("ErrExecShell", map[string]interface{}{"err": err.Error()}))
		} else {
			logger.Println(i18n.GetMsgByKey("ExecShellSuccess"))
		}
	}
	reloadSystemSSL(websiteSSL, logger)
	if websiteSSL.PushNode {
		if err = pushSSLToNode(websiteSSL, logger); err != nil {
			return nil, err
		}
	}
	return websiteSSL, nil
}

func createPrivateKey(keyType string) (privateKey any, publicKey any, privateKeyBytes []byte, err error) {
	privateKey, err = certcrypto.GeneratePrivateKey(ssl.KeyType(keyType))
	if err != nil {
		return
	}
	var (
		caPrivateKeyPEM = new(bytes.Buffer)
	)
	if ssl.KeyType(keyType) == certcrypto.EC256 || ssl.KeyType(keyType) == certcrypto.EC384 {
		publicKey = &privateKey.(*ecdsa.PrivateKey).PublicKey
		publicKey = publicKey.(*ecdsa.PublicKey)
		block := &pem.Block{
			Type: "EC PRIVATE KEY",
		}
		privateBytes, sErr := x509.MarshalECPrivateKey(privateKey.(*ecdsa.PrivateKey))
		if sErr != nil {
			err = sErr
			return
		}
		block.Bytes = privateBytes
		_ = pem.Encode(caPrivateKeyPEM, block)
	} else {
		publicKey = &privateKey.(*rsa.PrivateKey).PublicKey
		_ = pem.Encode(caPrivateKeyPEM, &pem.Block{
			Type:  "RSA PRIVATE KEY",
			Bytes: x509.MarshalPKCS1PrivateKey(privateKey.(*rsa.PrivateKey)),
		})
	}
	privateKeyBytes = caPrivateKeyPEM.Bytes()
	return
}

func (w WebsiteCAService) DownloadFile(id uint) (*os.File, error) {
	ca, err := websiteCARepo.GetFirst(repo.WithByID(id))
	if err != nil {
		return nil, err
	}
	fileOp := files.NewFileOp()
	dir := path.Join(global.Dir.DataDir, "tmp/ssl", ca.Name)
	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, "ca.crt"), strings.NewReader(ca.CSR), constant.DirPerm); err != nil {
		return nil, err
	}
	if err = fileOp.WriteFile(path.Join(dir, "ca.key"), strings.NewReader(ca.PrivateKey), constant.DirPerm); err != nil {
		return nil, err
	}
	fileName := ca.Name + ".zip"
	if err = fileOp.Compress(context.Background(), []string{path.Join(dir, "ca.crt"), path.Join(dir, "ca.key")}, dir, fileName, files.SdkZip, "", nil); err != nil {
		return nil, err
	}
	return os.Open(path.Join(dir, fileName))
}
