package baotapanelgo

import (
	"context"
	"crypto/sha256"
	"crypto/tls"
	"encoding/hex"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"github.com/samber/lo"

	"github.com/certimate-go/certimate/pkg/core"
	btpanelgosdk "github.com/certimate-go/certimate/pkg/sdk3rd/btpanelgo"
	xcert "github.com/certimate-go/certimate/pkg/utils/cert"
	xloop "github.com/certimate-go/certimate/pkg/utils/loop"
	xwait "github.com/certimate-go/certimate/pkg/utils/wait"
)

type (
	Provider     = core.Deployer
	DeployResult = core.DeployerDeployResult
)

type DeployerConfig struct {
	// 宝塔面板服务地址。
	ServerUrl string `json:"serverUrl"`
	// 宝塔面板接口密钥。
	ApiKey string `json:"apiKey"`
	// 是否允许不安全的连接。
	AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
	// 网站类型。
	SiteType string `json:"siteType"`
	// 网站名称。
	SiteNames []string `json:"siteNames,omitempty"`
}

type Deployer struct {
	config    *DeployerConfig
	logger    *slog.Logger
	sdkClient *btpanelgosdk.Client
}

var _ Provider = (*Deployer)(nil)

var (
	btProjectTypes      = []string{"php", "java", "asp", "go", "python", "nodejs", "proxy", "general"}
	btProjectTypesInIIS = []string{"php", "asp", "aspx"}
)

func NewDeployer(config *DeployerConfig) (*Deployer, error) {
	if config == nil {
		return nil, fmt.Errorf("the configuration of the deployer provider is nil")
	}

	client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)
	if err != nil {
		return nil, fmt.Errorf("could not create client: %w", err)
	}

	return &Deployer{
		config:    config,
		logger:    slog.Default(),
		sdkClient: client,
	}, nil
}

func (d *Deployer) SetLogger(logger *slog.Logger) {
	if logger == nil {
		d.logger = slog.New(slog.DiscardHandler)
	} else {
		d.logger = logger
	}
}

func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) {
	if len(d.config.SiteNames) == 0 {
		return nil, fmt.Errorf("config `siteNames` is required")
	}

	if d.config.SiteType != "" {
		if !lo.Contains(btProjectTypes, d.config.SiteType) && !lo.Contains(btProjectTypesInIIS, d.config.SiteType) {
			return nil, fmt.Errorf("unsupported site type: '%s'", d.config.SiteType)
		}
	}

	// 批量更新站点证书
	if err := xloop.ForRangeAllWithContext(ctx, d.config.SiteNames, func(ctx context.Context, siteName string, i int) error {
		if i > 0 {
			if err := xwait.DelayWithContext(ctx, 3*time.Second); err != nil {
				return err
			}
		}

		return d.updateSiteCertificate(ctx, d.config.SiteType, siteName, certPEM, privkeyPEM)
	}); err != nil {
		return nil, err
	}

	return &DeployResult{}, nil
}

func (d *Deployer) findSiteByName(ctx context.Context, siteType, siteName string) (*btpanelgosdk.SiteData, error) {
	if siteType == "" || lo.Contains(btProjectTypesInIIS, siteType) {
		// 查询网站列表
		datalistGetDataListPage := 1
		datalistGetDataListLimit := 10
		for {
			select {
			case <-ctx.Done():
				return nil, ctx.Err()
			default:
			}

			datalistGetDataListReq := &btpanelgosdk.DatalistGetDataListRequest{
				Table:        lo.ToPtr("sites"),
				SearchString: lo.ToPtr(siteName),
				Page:         lo.ToPtr(int32(datalistGetDataListPage)),
				Limit:        lo.ToPtr(int32(datalistGetDataListLimit)),
			}
			datalistGetDataListResp, err := d.sdkClient.DatalistGetDataListWithContext(ctx, datalistGetDataListReq)
			d.logger.Debug("sdk request 'datalist.GetDataList'", slog.Any("request", datalistGetDataListReq), slog.Any("response", datalistGetDataListResp))
			if err != nil {
				return nil, fmt.Errorf("failed to execute sdk request 'datalist.GetDataList': %w", err)
			}

			for _, siteItem := range datalistGetDataListResp.Data {
				if strings.EqualFold(siteItem.Name, siteName) {
					return siteItem, nil
				}
			}

			if len(datalistGetDataListResp.Data) < datalistGetDataListLimit {
				break
			}

			datalistGetDataListPage++
		}
	} else {
		// 查询网站列表
		siteGetProjectListPage := 1
		siteGetProjectListLimit := 10
		for {
			select {
			case <-ctx.Done():
				return nil, ctx.Err()
			default:
			}

			siteGetProjectListReq := &btpanelgosdk.SiteGetProjectListRequest{
				SearchType:   lo.ToPtr(siteType),
				SearchString: lo.ToPtr(siteName),
				Page:         lo.ToPtr(int32(siteGetProjectListPage)),
				Limit:        lo.ToPtr(int32(siteGetProjectListLimit)),
			}
			siteGetProjectListResp, err := d.sdkClient.SiteGetProjectListWithContext(ctx, siteGetProjectListReq)
			d.logger.Debug("sdk request 'site.GetProjectList'", slog.Any("request", siteGetProjectListReq), slog.Any("response", siteGetProjectListResp))
			if err != nil {
				return nil, fmt.Errorf("failed to execute sdk request 'site.GetProjectList': %w", err)
			}

			for _, siteItem := range siteGetProjectListResp.Data {
				if strings.EqualFold(siteItem.Name, siteName) {
					return siteItem, nil
				}
			}

			if len(siteGetProjectListResp.Data) < siteGetProjectListLimit {
				break
			}

			siteGetProjectListPage++
		}
	}

	return nil, fmt.Errorf("could not find site '%s'", siteName)
}

func (d *Deployer) updateSiteCertificate(ctx context.Context, siteType, siteName string, certPEM, privkeyPEM string) error {
	// 获取面板配置
	panelGetConfigReq := &btpanelgosdk.PanelGetConfigRequest{}
	panelGetConfigResp, err := d.sdkClient.PanelGetConfigWithContext(ctx, panelGetConfigReq)
	d.logger.Debug("sdk request 'panel.GetConfig'", slog.Any("request", panelGetConfigReq), slog.Any("response", panelGetConfigResp))
	if err != nil {
		return fmt.Errorf("failed to execute sdk request 'panel.GetConfig': %w", err)
	}

	// 获取网站
	siteData, err := d.findSiteByName(ctx, siteType, siteName)
	if err != nil {
		return err
	}

	// 根据服务器类型部署证书
	pfxRequried := panelGetConfigResp.Site != nil && strings.EqualFold(panelGetConfigResp.Site.WebServer, "iis")
	if pfxRequried {
		// 转换证书格式
		certPFXPassword := "certimate"
		certPFX, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, certPFXPassword, nil)
		if err != nil {
			return fmt.Errorf("failed to transform certificate from PEM to PFX: %w", err)
		}

		// 上传证书
		certPFXHash := sha256.Sum256([]byte(certPFX))
		certPFXHashHex := hex.EncodeToString(certPFXHash[:])
		certPFXPath := panelGetConfigResp.Paths.Soft + "/temp/ssl/certimate"
		certPFXFileName := fmt.Sprintf("%s.pfx", certPFXHashHex)
		filesUploadReq := &btpanelgosdk.FilesUploadRequest{
			Path:  lo.ToPtr(certPFXPath),
			Name:  lo.ToPtr(certPFXFileName),
			Start: lo.ToPtr(int32(0)),
			Size:  lo.ToPtr(int32(len(certPFX))),
			Blob:  certPFX,
			Force: lo.ToPtr(true),
		}
		filesUploadResp, err := d.sdkClient.FilesUploadWithContext(ctx, filesUploadReq)
		d.logger.Debug("sdk request 'files.Upload'", slog.Any("request", filesUploadReq), slog.Any("response", filesUploadResp))
		if err != nil {
			return fmt.Errorf("failed to execute sdk request 'files.Upload': %w", err)
		}

		// 服务器为 IIS，设置网站 SSL
		siteSetSitePFXSSLReq := &btpanelgosdk.SiteSetSitePFXSSLRequest{
			SiteId:   lo.ToPtr(siteData.Id),
			PFX:      lo.ToPtr(fmt.Sprintf("%s/%s", certPFXPath, certPFXFileName)),
			Password: lo.ToPtr(certPFXPassword),
		}
		siteSetSitePFXSSLResp, err := d.sdkClient.SiteSetSitePFXSSLWithContext(ctx, siteSetSitePFXSSLReq)
		d.logger.Debug("sdk request 'site.SetSitePFXSSL'", slog.Any("request", siteSetSitePFXSSLReq), slog.Any("response", siteSetSitePFXSSLResp))
		if err != nil {
			return fmt.Errorf("failed to execute sdk request 'site.SetSitePFXSSL': %w", err)
		}
	} else {
		// 服务器非 IIS，设置网站 SSL
		siteSetSiteSSLReq := &btpanelgosdk.SiteSetSiteSSLRequest{
			SiteId: lo.ToPtr(siteData.Id),
			Status: lo.ToPtr(true),
			Key:    lo.ToPtr(privkeyPEM),
			Cert:   lo.ToPtr(certPEM),
		}
		siteSetSiteSSLResp, err := d.sdkClient.SiteSetSiteSSLWithContext(ctx, siteSetSiteSSLReq)
		d.logger.Debug("sdk request 'site.SetSiteSSL'", slog.Any("request", siteSetSiteSSLReq), slog.Any("response", siteSetSiteSSLResp))
		if err != nil {
			return fmt.Errorf("failed to execute sdk request 'site.SetSiteSSL': %w", err)
		}
	}

	return nil
}

func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btpanelgosdk.Client, error) {
	client, err := btpanelgosdk.NewClient(serverUrl,
		btpanelgosdk.WithApiKey(apiKey),
	)
	if err != nil {
		return nil, err
	}

	if skipTlsVerify {
		client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
	}

	return client, nil
}
