package nginxproxymanager

import (
	"context"
	"crypto/tls"
	"fmt"
	"log/slog"
	"strconv"
	"time"

	"github.com/samber/lo"

	"github.com/certimate-go/certimate/pkg/core"
	cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/nginxproxymanager"
	npmsdk "github.com/certimate-go/certimate/pkg/sdk3rd/nginxproxymanager"
	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 {
	// NPM 服务地址。
	ServerUrl string `json:"serverUrl"`
	// NPM API 认证方式。
	// 可取值 "password"、"token"。
	// 零值时默认值 [AUTH_METHOD_PASSWORD]。
	AuthMethod string `json:"authMethod,omitempty"`
	// NPM 用户名。
	Username string `json:"username,omitempty"`
	// NPM 密码。
	Password string `json:"password,omitempty"`
	// NPM API Token。
	ApiToken string `json:"apiToken,omitempty"`
	// 是否允许不安全的连接。
	AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
	// 部署目标。
	DeployTarget string `json:"deployTarget"`
	// 域名匹配模式。
	// 零值时默认值 [HOST_MATCH_PATTERN_SPECIFIED]。
	HostMatchPattern string `json:"hostMatchPattern,omitempty"`
	// 主机类型。
	// 部署目标为 [DEPLOY_TARGET_HOST] 时必填。
	HostType string `json:"hostType,omitempty"`
	// 主机 ID。
	// 部署目标为 [DEPLOY_TARGET_HOST]、且匹配模式非 [HOST_MATCH_PATTERN_CERTSAN] 时必填。
	HostId int64 `json:"hostId,omitempty"`
	// 证书 ID。
	// 部署目标为 [DEPLOY_TARGET_CERTIFICATE] 时必填。
	CertificateId int64 `json:"certificateId,omitempty"`
}

type Deployer struct {
	config     *DeployerConfig
	logger     *slog.Logger
	sdkClient  *npmsdk.Client
	sdkCertmgr core.Certmgr
}

var _ Provider = (*Deployer)(nil)

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.AuthMethod, config.Username, config.Password, config.ApiToken, config.AllowInsecureConnections)
	if err != nil {
		return nil, fmt.Errorf("could not create client: %w", err)
	}

	pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{
		ServerUrl:                config.ServerUrl,
		AuthMethod:               config.AuthMethod,
		Username:                 config.Username,
		Password:                 config.Password,
		ApiToken:                 config.ApiToken,
		AllowInsecureConnections: config.AllowInsecureConnections,
	})
	if err != nil {
		return nil, fmt.Errorf("could not create certmgr: %w", err)
	}

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

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

	d.sdkCertmgr.SetLogger(logger)
}

func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) {
	// 根据部署目标决定业务流程
	switch d.config.DeployTarget {
	case DEPLOY_TARGET_HOST:
		if err := d.deployToHost(ctx, certPEM, privkeyPEM); err != nil {
			return nil, err
		}

	case DEPLOY_TARGET_CERTIFICATE:
		if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {
			return nil, err
		}

	default:
		return nil, fmt.Errorf("unsupported deploy target '%s'", d.config.DeployTarget)
	}

	return &DeployResult{}, nil
}

func (d *Deployer) deployToHost(ctx context.Context, certPEM, privkeyPEM string) error {
	// 解析证书内容
	certX509, err := xcert.ParseCertificateFromPEM(certPEM)
	if err != nil {
		return err
	}

	// 上传证书
	upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)
	if err != nil {
		return fmt.Errorf("failed to upload certificate file: %w", err)
	} else {
		d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
	}

	// 获取全部可部署的主机列表
	hostsByType, err := d.getAllHosts(ctx, d.config.HostType)
	if err != nil {
		return err
	}

	// 获取待部署的主机列表
	var hostIds []int64
	switch d.config.HostMatchPattern {
	case "", HOST_MATCH_PATTERN_SPECIFIED:
		{
			if d.config.HostId == 0 {
				return fmt.Errorf("config `hostId` is required")
			}

			hostIds = []int64{d.config.HostId}
		}

	case HOST_MATCH_PATTERN_CERTSAN:
		{
			hostCandidates := hostsByType
			hostIds = lo.Map(
				lo.Filter(hostCandidates, func(hostItem *npmsdk.Host, _ int) bool {
					return len(hostItem.DomainNames) > 0 &&
						lo.EveryBy(hostItem.DomainNames, func(domain string) bool {
							return certX509.VerifyHostname(domain) == nil
						})
				}),
				func(hostItem *npmsdk.Host, _ int) int64 {
					return hostItem.Id
				},
			)
			if len(hostIds) == 0 {
				return fmt.Errorf("could not find any hosts matched by certificate")
			}
		}

	default:
		return fmt.Errorf("unsupported host match pattern: '%s'", d.config.HostMatchPattern)
	}

	// 批量更新主机证书
	if len(hostIds) == 0 {
		d.logger.Info("no hosts to deploy")
	} else {
		d.logger.Info("found hosts to deploy", slog.Any("hostIds", hostIds))

		// 跳过已部署过的主机
		hostIds = lo.Filter(hostIds, func(hostId int64, _ int) bool {
			hostInfo, _ := lo.Find(hostsByType, func(hostItem *npmsdk.Host) bool {
				return hostId == hostItem.Id
			})
			if hostInfo != nil {
				certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
				return hostInfo.CertificateId != certId
			}

			return true
		})

		if err := xloop.ForRangeAllWithContext(ctx, hostIds, func(ctx context.Context, hostId int64, i int) error {
			if i > 0 {
				if err := xwait.DelayWithContext(ctx, 3*time.Second); err != nil {
					return err
				}
			}

			certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
			return d.updateHostCertificate(ctx, d.config.HostType, hostId, certId)
		}); err != nil {
			return err
		}
	}

	return nil
}

func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {
	if d.config.CertificateId == 0 {
		return fmt.Errorf("config `certificateId` is required")
	}

	// 替换证书
	rplres, err := d.sdkCertmgr.Replace(ctx, strconv.FormatInt(d.config.CertificateId, 10), certPEM, privkeyPEM)
	if err != nil {
		return fmt.Errorf("failed to replace certificate file: %w", err)
	} else {
		d.logger.Info("ssl certificate replaced", slog.Any("result", rplres))
	}

	// 获取默认站点
	settingsGetDefaultSiteReq := &npmsdk.SettingsGetDefaultSiteRequest{}
	settingsGetDefaultSiteResp, err := d.sdkClient.SettingsGetDefaultSiteWithContext(ctx, settingsGetDefaultSiteReq)
	d.logger.Debug("sdk request 'settings.GetDefaultSite'", slog.Any("request", settingsGetDefaultSiteReq), slog.Any("response", settingsGetDefaultSiteResp))
	if err != nil {
		return fmt.Errorf("failed to execute sdk request 'settings.GetDefaultSite': %w", err)
	}

	// 更新默认站点，以触发 nginx 重启
	settingsSetDefaultSiteReq := &npmsdk.SettingsSetDefaultSiteRequest{
		Value: settingsGetDefaultSiteResp.Value,
	}
	settingsSetDefaultSiteResp, err := d.sdkClient.SettingsSetDefaultSiteWithContext(ctx, settingsSetDefaultSiteReq)
	d.logger.Debug("sdk request 'settings.SetDefaultSite'", slog.Any("request", settingsSetDefaultSiteReq), slog.Any("response", settingsSetDefaultSiteResp))
	if err != nil {
		return fmt.Errorf("failed to execute sdk request 'settings.SetDefaultSite': %w", err)
	}

	return nil
}

func (d *Deployer) getAllHosts(ctx context.Context, cloudHostType string) ([]*npmsdk.Host, error) {
	var hosts []*npmsdk.Host
	switch cloudHostType {
	case HOST_TYPE_PROXY:
		{
			nginxListProxyHostsReq := &npmsdk.NginxListProxyHostsRequest{}
			nginxListProxyHostsResp, err := d.sdkClient.NginxListProxyHostsWithContext(ctx, nginxListProxyHostsReq)
			d.logger.Debug("sdk request 'nginx.ListProxyHosts'", slog.Any("request", nginxListProxyHostsReq), slog.Any("response", nginxListProxyHostsResp))
			if err != nil {
				return nil, fmt.Errorf("failed to execute sdk request 'nginx.ListProxyHosts': %w", err)
			}

			hosts = make([]*npmsdk.Host, 0, len(*nginxListProxyHostsResp))
			for _, hostItem := range *nginxListProxyHostsResp {
				hosts = append(hosts, &hostItem.Host)
			}
		}

	case HOST_TYPE_REDIRECTION:
		{
			nginxListRedirectionHostsReq := &npmsdk.NginxListRedirectionHostsRequest{}
			nginxListRedirectionHostsResp, err := d.sdkClient.NginxListRedirectionHostsWithContext(ctx, nginxListRedirectionHostsReq)
			d.logger.Debug("sdk request 'nginx.ListRedirectionHosts'", slog.Any("request", nginxListRedirectionHostsReq), slog.Any("response", nginxListRedirectionHostsResp))
			if err != nil {
				return nil, fmt.Errorf("failed to execute sdk request 'nginx.ListRedirectionHosts': %w", err)
			}

			hosts = make([]*npmsdk.Host, 0, len(*nginxListRedirectionHostsResp))
			for _, hostItem := range *nginxListRedirectionHostsResp {
				hosts = append(hosts, &hostItem.Host)
			}
		}

	case HOST_TYPE_STREAM:
		{
			nginxListStreamsReq := &npmsdk.NginxListStreamsRequest{}
			nginxListStreamsResp, err := d.sdkClient.NginxListStreamsWithContext(ctx, nginxListStreamsReq)
			d.logger.Debug("sdk request 'nginx.ListStreams'", slog.Any("request", nginxListStreamsReq), slog.Any("response", nginxListStreamsResp))
			if err != nil {
				return nil, fmt.Errorf("failed to execute sdk request 'nginx.ListStreams': %w", err)
			}

			hosts = make([]*npmsdk.Host, 0, len(*nginxListStreamsResp))
			for _, hostItem := range *nginxListStreamsResp {
				hosts = append(hosts, &hostItem.Host)
			}
		}

	case HOST_TYPE_DEAD:
		{
			nginxListDeadHostsReq := &npmsdk.NginxListDeadHostsRequest{}
			nginxListDeadHostsResp, err := d.sdkClient.NginxListDeadHostsWithContext(ctx, nginxListDeadHostsReq)
			d.logger.Debug("sdk request 'nginx.ListDeadHosts'", slog.Any("request", nginxListDeadHostsReq), slog.Any("response", nginxListDeadHostsResp))
			if err != nil {
				return nil, fmt.Errorf("failed to execute sdk request 'nginx.ListDeadHosts': %w", err)
			}

			hosts = make([]*npmsdk.Host, 0, len(*nginxListDeadHostsResp))
			for _, hostItem := range *nginxListDeadHostsResp {
				hosts = append(hosts, &hostItem.Host)
			}
		}

	default:
		return hosts, fmt.Errorf("unsupported host type: '%s'", cloudHostType)
	}

	return hosts, nil
}

func (d *Deployer) updateHostCertificate(ctx context.Context, cloudHostType string, cloudHostId int64, cloudCertId int64) error {
	switch cloudHostType {
	case HOST_TYPE_PROXY:
		{
			nginxUpdateProxyHostReq := &npmsdk.NginxUpdateProxyHostRequest{
				CertificateId: lo.ToPtr(cloudCertId),
			}
			nginxUpdateProxyHostResp, err := d.sdkClient.NginxUpdateProxyHostWithContext(ctx, cloudHostId, nginxUpdateProxyHostReq)
			d.logger.Debug("sdk request 'nginx.UpdateProxyHost'", slog.Int64("params.hostId", cloudHostId), slog.Any("request", nginxUpdateProxyHostReq), slog.Any("response", nginxUpdateProxyHostResp))
			if err != nil {
				return fmt.Errorf("failed to execute sdk request 'nginx.UpdateProxyHost': %w", err)
			}
		}

	case HOST_TYPE_REDIRECTION:
		{
			nginxUpdateRedirectionHostReq := &npmsdk.NginxUpdateRedirectionHostRequest{
				CertificateId: lo.ToPtr(cloudCertId),
			}
			nginxUpdateRedirectionHostResp, err := d.sdkClient.NginxUpdateRedirectionHostWithContext(ctx, cloudHostId, nginxUpdateRedirectionHostReq)
			d.logger.Debug("sdk request 'nginx.UpdateRedirectionHost'", slog.Int64("params.hostId", cloudHostId), slog.Any("request", nginxUpdateRedirectionHostReq), slog.Any("response", nginxUpdateRedirectionHostResp))
			if err != nil {
				return fmt.Errorf("failed to execute sdk request 'nginx.UpdateRedirectionHost': %w", err)
			}
		}

	case HOST_TYPE_STREAM:
		{
			nginxUpdateStreamReq := &npmsdk.NginxUpdateStreamRequest{
				CertificateId: lo.ToPtr(cloudCertId),
			}
			nginxUpdateStreamResp, err := d.sdkClient.NginxUpdateStreamWithContext(ctx, cloudHostId, nginxUpdateStreamReq)
			d.logger.Debug("sdk request 'nginx.UpdateStream'", slog.Int64("params.hostId", cloudHostId), slog.Any("request", nginxUpdateStreamReq), slog.Any("response", nginxUpdateStreamResp))
			if err != nil {
				return fmt.Errorf("failed to execute sdk request 'nginx.UpdateStream': %w", err)
			}
		}

	case HOST_TYPE_DEAD:
		{
			nginxUpdateDeadHostReq := &npmsdk.NginxUpdateDeadHostRequest{
				CertificateId: lo.ToPtr(cloudCertId),
			}
			nginxUpdateDeadHostResp, err := d.sdkClient.NginxUpdateDeadHostWithContext(ctx, cloudHostId, nginxUpdateDeadHostReq)
			d.logger.Debug("sdk request 'nginx.UpdateDeadHost'", slog.Int64("params.hostId", cloudHostId), slog.Any("request", nginxUpdateDeadHostReq), slog.Any("response", nginxUpdateDeadHostResp))
			if err != nil {
				return fmt.Errorf("failed to execute sdk request 'nginx.UpdateDeadHost': %w", err)
			}
		}

	default:
		return fmt.Errorf("unsupported host type: '%s'", cloudHostType)
	}

	return nil
}

func createSDKClient(serverUrl, authMethod, username, password, apiToken string, skipTlsVerify bool) (*npmsdk.Client, error) {
	var client *npmsdk.Client
	var err error

	switch authMethod {
	case "", AUTH_METHOD_PASSWORD:
		{
			client, err = npmsdk.NewClient(serverUrl,
				npmsdk.WithLogins(username, password),
			)
		}

	case AUTH_METHOD_TOKEN:
		{
			client, err = npmsdk.NewClient(serverUrl,
				npmsdk.WithJwtToken(apiToken),
			)
		}
	}

	if err != nil {
		return nil, err
	}

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

	return client, nil
}
