// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cert

import (
	"context"
	"fmt"
	"os"
	"reflect"
	"sync"

	"github.com/caddyserver/certmagic"
	"github.com/mholt/acmez"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	istiomodel "istio.io/istio/pilot/pkg/model"
	"k8s.io/client-go/kubernetes"
)

const (
	EventCertObtained = "cert_obtained"
)

var (
	cfg *certmagic.Config
)

type CertMgr struct {
	cfg           *certmagic.Config
	client        kubernetes.Interface
	namespace     string
	mux           sync.RWMutex
	storage       certmagic.Storage
	cache         *certmagic.Cache
	myACME        *certmagic.ACMEIssuer
	ingressSolver acmez.Solver
	configMgr     *ConfigMgr
	secretMgr     *SecretMgr
	XDSUpdater    istiomodel.XDSUpdater
}

func InitCertMgr(opts *Option, clientSet kubernetes.Interface, config *Config, XDSUpdater istiomodel.XDSUpdater, configMgr *ConfigMgr) (*CertMgr, error) {
	CertLog.Infof("certmgr init config: %+v", config)
	// Init certmagic config
	// First make a pointer to a Cache as we need to reference the same Cache in
	// GetConfigForCert below.
	var cache *certmagic.Cache
	var storage certmagic.Storage
	storage, _ = NewConfigmapStorage(opts.Namespace, clientSet)
	renewalWindowRatio := float64(config.RenewBeforeDays) / float64(RenewMaxDays)
	logger := zap.New(zapcore.NewCore(
		zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
		os.Stderr,
		zap.DebugLevel,
	))
	magicConfig := certmagic.Config{
		RenewalWindowRatio: renewalWindowRatio,
		Storage:            storage,
		Logger:             logger,
	}
	cache = certmagic.NewCache(certmagic.CacheOptions{
		GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
			// Here we use New to get a valid Config associated with the same cache.
			// The provided Config is used as a template and will be completed with
			// any defaults that are set in the Default config.
			return cfg, nil
		},
		Logger: logger,
	})
	// init certmagic
	cfg = certmagic.New(cache, magicConfig)

	// Init certmagic acme
	issuer := config.GetIssuer(IssuerTypeLetsencrypt)
	if issuer == nil {
		// should never happen here
		return nil, fmt.Errorf("there is no Letsencrypt Issuer found in config")
	}

	myACME := certmagic.NewACMEIssuer(cfg, certmagic.ACMEIssuer{
		//CA:                      certmagic.LetsEncryptStagingCA,
		CA:                      certmagic.LetsEncryptProductionCA,
		Email:                   issuer.Email,
		Agreed:                  true,
		DisableHTTPChallenge:    false,
		DisableTLSALPNChallenge: true,
	})
	// inject http01 solver
	ingressSolver, _ := NewIngressSolver(opts.Namespace, clientSet, myACME)
	myACME.Http01Solver = ingressSolver
	// init issuers
	cfg.Issuers = []certmagic.Issuer{myACME}

	secretMgr, _ := NewSecretMgr(opts.Namespace, clientSet)

	certMgr := &CertMgr{
		cfg:           cfg,
		client:        clientSet,
		namespace:     opts.Namespace,
		myACME:        myACME,
		ingressSolver: ingressSolver,
		configMgr:     configMgr,
		secretMgr:     secretMgr,
		cache:         cache,
		XDSUpdater:    XDSUpdater,
	}
	certMgr.cfg.OnEvent = certMgr.OnEvent
	return certMgr, nil
}
func (s *CertMgr) Reconcile(ctx context.Context, oldConfig *Config, newConfig *Config) error {
	CertLog.Infof("cermgr reconcile old config:%+v to new config:%+v", oldConfig, newConfig)
	// sync email
	if oldConfig != nil && newConfig != nil {
		oldIssuer := oldConfig.GetIssuer(IssuerTypeLetsencrypt)
		newIssuer := newConfig.GetIssuer(IssuerTypeLetsencrypt)
		if oldIssuer.Email != newIssuer.Email {
			// TODO before sync email, maybe need to clean up cache and account
		}
	}

	// sync domains
	newDomains := make([]string, 0)
	newDomainsMap := make(map[string]string, 0)
	removeDomains := make([]string, 0)

	if newConfig != nil {
		for _, config := range newConfig.CredentialConfig {
			if config.TLSIssuer == IssuerTypeLetsencrypt {
				for _, newDomain := range config.Domains {
					newDomains = append(newDomains, newDomain)
					newDomainsMap[newDomain] = newDomain
				}

			}
		}
	}

	if oldConfig != nil {
		for _, config := range oldConfig.CredentialConfig {
			if config.TLSIssuer == IssuerTypeLetsencrypt {
				for _, oldDomain := range config.Domains {
					if _, ok := newDomainsMap[oldDomain]; !ok {
						removeDomains = append(removeDomains, oldDomain)
					}
				}

			}
		}
	}

	if newConfig.AutomaticHttps == true {
		newIssuer := newConfig.GetIssuer(IssuerTypeLetsencrypt)
		// clean up  unused domains
		s.cleanSync(context.Background(), removeDomains)
		// sync email
		s.myACME.Email = newIssuer.Email
		// sync RenewalWindowRatio
		renewalWindowRatio := float64(newConfig.RenewBeforeDays) / float64(RenewMaxDays)
		s.cfg.RenewalWindowRatio = renewalWindowRatio
		// start cache
		s.cache.Start()
		// sync domains
		s.configMgr.SetConfig(newConfig)
		CertLog.Infof("certMgr start to manageSync domains: %+v", newDomains)
		s.manageSync(context.Background(), newDomains)
		CertLog.Infof("certMgr manageSync domains done")
	} else {
		// stop cache  maintainAssets
		s.cache.Stop()
		s.configMgr.SetConfig(newConfig)
	}

	if oldConfig != nil && newConfig != nil {
		if oldConfig.FallbackForInvalidSecret != newConfig.FallbackForInvalidSecret || !reflect.DeepEqual(oldConfig.CredentialConfig, newConfig.CredentialConfig) {
			CertLog.Infof("ingress need to full push")
			s.XDSUpdater.ConfigUpdate(&istiomodel.PushRequest{
				Full:   true,
				Reason: istiomodel.NewReasonStats("higress-https-updated"),
			})
		}
	}

	return nil
}

func (s *CertMgr) manageSync(ctx context.Context, domainNames []string) error {
	CertLog.Infof("cert manage sync domains:%v", domainNames)
	return s.cfg.ManageSync(ctx, domainNames)
}

func (s *CertMgr) cleanSync(ctx context.Context, domainNames []string) error {
	//TODO implement clean up domains
	CertLog.Infof("cert clean sync domains:%v", domainNames)
	return nil
}

func (s *CertMgr) OnEvent(ctx context.Context, event string, data map[string]any) error {
	CertLog.Infof("certmgr receive event:% data:%+v", event, data)
	/**
	event: cert_obtained
	cfg.emit(ctx, "cert_obtained", map[string]any{
		"renewal":          true,
		"remaining":        timeLeft,
		"identifier":       name,
		"issuer":           issuerKey,
		"storage_path":     StorageKeys.CertsSitePrefix(issuerKey, certKey),
		"private_key_path": StorageKeys.SitePrivateKey(issuerKey, certKey),
		"certificate_path": StorageKeys.SiteCert(issuerKey, certKey),
		"metadata_path":    StorageKeys.SiteMeta(issuerKey, certKey),
	})
	*/
	if event == EventCertObtained {
		// obtain certificate and update secret
		domain := data["identifier"].(string)
		isRenew := data["renewal"].(bool)
		privateKeyPath := data["private_key_path"].(string)
		certificatePath := data["certificate_path"].(string)
		privateKey, err := s.cfg.Storage.Load(context.Background(), privateKeyPath)
		certificate, err := s.cfg.Storage.Load(context.Background(), certificatePath)
		certChain, err := parseCertsFromPEMBundle(certificate)
		if err != nil {
			return err
		}
		notAfterTime := notAfter(certChain[0])
		notBeforeTime := notBefore(certChain[0])
		secretName := s.configMgr.GetConfig().GetSecretNameByDomain(IssuerTypeLetsencrypt, domain)
		if len(secretName) == 0 {
			CertLog.Errorf("can not find secret name for domain % in config", domain)
			return nil
		}
		err2 := s.secretMgr.Update(domain, secretName, privateKey, certificate, notBeforeTime, notAfterTime, isRenew)
		if err2 != nil {
			CertLog.Errorf("update secretName %s for domain %s error: %v", secretName, domain, err2)
		}
		return err
	}
	return nil
}
