package controller

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"io/fs"
	"io/ioutil"
	"net/url"
	"os"
	"path/filepath"
	"strings"

	"chaitin.cn/patronus/safeline-2/management/tcontrollerd/model"
	"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/ngcmd"
	pb "chaitin.cn/patronus/safeline-2/management/tcontrollerd/proto/website"
	"chaitin.cn/patronus/safeline-2/management/tcontrollerd/utils"
)

const (
	Ping                   = "ping"
	Pong                   = "pong"
	EventTypeWebsite       = "website"
	EventTypeDeleteWebsite = "deleteWebsite"
	EventTypeFullWebsite   = "fullWebsite"

	nginxConfigPath       = "/etc/nginx/sites-enabled/" // 修改的为 host /resources/nginx/ 中的文件
	nginxFilePrefix       = "IF_backend_"
	nginxBackupFilePrefix = "BAK_IF_backend_"
	nginxFileMode         = 0644

	HttpsScheme      = "https"
	DefaultHttpsPort = "443"
)

var pong = pb.Response{Type: Pong, Msg: nil, Err: false}

func generateNginxConfig(website *model.WebsiteConfig) (string, error) {
	// only ONE upstream supported in v1.0
	var upstreamAddr string
	scheme := "http"
	for _, upstream := range website.Upstreams {
		urlInfo, err := url.Parse(upstream)
		if err != nil {
			return "", err
		}
		if urlInfo.Scheme == HttpsScheme && urlInfo.Port() == "" {
			urlInfo.Host = urlInfo.Host + ":" + DefaultHttpsPort
		}
		upstreamAddr = fmt.Sprintf(upstreamAddrTpl, urlInfo.Host)
		if len(urlInfo.Scheme) > 0 {
			scheme = urlInfo.Scheme
		}
	}

	sslFlag := ""
	sslCertFilename := ""
	sslCertKeyFilename := ""
	if website.KeyFilename != "" && website.CertFilename != "" {
		sslFlag = " ssl" // with a blank ahead
		sslCertFilename = fmt.Sprintf(certTpl, website.CertFilename)
		sslCertKeyFilename = fmt.Sprintf(certKeyTpl, website.KeyFilename)
	}

	// only ONE server name supported in v1.0
	var serverName string
	var addrAnyProperties string
	for _, sn := range website.ServerNames {
		if sn == "*" || sn == "" {
			sn = "_"
			addrAnyProperties = addrAnyPropertiesTpl
		}
		serverName = fmt.Sprintf(serverNameTpl, sn)
	}

	// only ONE port supported in v1.0
	var serverListen string
	for _, port := range website.Ports {
		serverListen = fmt.Sprintf(serverListenTpl, port, sslFlag, addrAnyProperties)
	}

	// Rate limiting directives
	rateLimitDirective := ""
	if website.RateLimitEnabled && website.RateLimitRPS > 0 {
		zoneName := fmt.Sprintf("site_%d", website.Id)
		rps := website.RateLimitRPS
		burst := website.RateLimitBurst
		if burst <= 0 {
			burst = rps * 2
		}
		rateLimitDirective = fmt.Sprintf(rateLimitReqTpl, zoneName, burst)
	}

	// Challenge directive
	challengeDirective := ""
	if website.ChallengeEnabled {
		challengeDirective = `error_page 403 = /__waf_challenge;`
	}

	upstreamName := fmt.Sprintf(backendTpl, website.Id)
	nginxConfig := strings.TrimSpace(fmt.Sprintf(nginxConfigTpl,
		upstreamName, upstreamAddr,
		serverListen, serverName, sslCertFilename, sslCertKeyFilename,
		scheme, upstreamName, website.Id,
		rateLimitDirective, challengeDirective,
	))

	// Prepend rate limit zone definition if enabled
	if website.RateLimitEnabled && website.RateLimitRPS > 0 {
		zoneName := fmt.Sprintf("site_%d", website.Id)
		rps := website.RateLimitRPS
		zoneDef := fmt.Sprintf(rateLimitZoneTpl, zoneName, 10, fmt.Sprintf("%dr/s", rps))
		nginxConfig = zoneDef + "\n" + nginxConfig
	}

	return nginxConfig, nil
}

func nginxTestAndReload() error {
	err := ngcmd.NginxConfTest()
	if err != nil {
		return err
	}

	err = ngcmd.NginxConfReload()
	if err != nil {
		return err
	}

	return nil
}

func generateFullConfigAndReload(msg []byte) error {
	var websites []model.WebsiteConfig
	if err := json.Unmarshal(msg, &websites); err != nil {
		return err
	}

	configFilename := make(map[string]struct{})
	for _, website := range websites {
		configFilename[fmt.Sprintf("%s%d", nginxFilePrefix, website.Id)] = struct{}{}
	}
	filepath.Walk(nginxConfigPath, func(path string, info fs.FileInfo, err error) error {
		_, ok := configFilename[info.Name()]
		if !ok && strings.HasPrefix(info.Name(), nginxFilePrefix) {
			if err := os.Remove(path); err != nil {
				// not return error only logged error in order to delete not exist website nginx conf
				logger.Warn(err)
			}
		}

		return nil
	})

	for _, website := range websites {
		if err := generateConfigAndReload(&website); err != nil {
			// trigger a full site push when tcd starts, ignore some site errors, and push as much as possible
			logger.Warn(err)
		}
	}

	return nil
}

func generateConfigAndReload(website *model.WebsiteConfig) error {
	nginxConfig, err := generateNginxConfig(website)
	if err != nil {
		return err
	}

	configPath := filepath.Join(nginxConfigPath, fmt.Sprintf("%s%d", nginxFilePrefix, website.Id))
	backupPath := filepath.Join(nginxConfigPath, fmt.Sprintf("%s%d", nginxBackupFilePrefix, website.Id))

	oldConfigExists, err := utils.FileExist(configPath)
	if err != nil {
		return err
	}

	if oldConfigExists {
		oldConfig, err := ioutil.ReadFile(configPath)
		if err != nil {
			return err
		}

		if string(oldConfig) == nginxConfig {
			logger.Info("No changes in the new website config, skip nginx -s reload")
			return nil
		}

		// tmp save old config to the backup path
		if err = utils.CopyFile(configPath, backupPath); err != nil {
			return err
		}
	}

	if err = utils.EnsureWriteFile(configPath, []byte(nginxConfig), nginxFileMode); err != nil {
		return err
	}

	if err = nginxTestAndReload(); err != nil {
		nginxError := err
		if err = os.Remove(configPath); err != nil {
			return err
		}
		if oldConfigExists {
			// new config err, restore old config
			if err = utils.CopyFile(backupPath, configPath); err != nil {
				return err
			}
			if err = os.Remove(backupPath); err != nil {
				return err
			}
		}
		return nginxError
	}

	if oldConfigExists {
		if err = os.Remove(backupPath); err != nil {
			return err
		}
	}

	return nil
}

func deleteConfigAndReload(config []byte) error {
	var websiteIds []uint
	if err := json.Unmarshal(config, &websiteIds); err != nil {
		return err
	}

	for _, id := range websiteIds {
		configPath := filepath.Join(nginxConfigPath, fmt.Sprintf("%s%d", nginxFilePrefix, id))
		exists, err := utils.FileExist(configPath)
		if err != nil {
			return err
		}
		if exists {
			if err := os.Remove(configPath); err != nil {
				return err
			}
		}
	}

	if err := nginxTestAndReload(); err != nil {
		return err
	}

	return nil
}

func sendResponse(stream pb.Website_SubscribeClient, eventType string, errMsg []byte) error {
	return stream.Send(&pb.Response{
		Type: eventType,
		Msg:  errMsg,
		Err:  len(errMsg) != 0,
	})
}

func websiteHandler(wc pb.WebsiteClient) error {
	stream, err := wc.Subscribe(context.Background())
	if err != nil {
		logger.Errorf("Subscribe failed: %v", err)
		return err
	}

	for {
		event, err := stream.Recv()
		if err != nil {
			if err == io.EOF {
				// read done.
				logger.Infof("Recv EOF from webserver")
				return nil
			} else {
				logger.Errorf("Handle failed: %v", err)
				return err
			}
		}
		logger.Debugf("Got message Type %s, Msg: %s", event.GetType(), event.GetMsg())
		if event.Type == Ping {
			if err = stream.Send(&pong); err != nil {
				return err
			}
		} else if event.Type == EventTypeWebsite {
			logger.Infof("Update website with config: %s", event.GetMsg())
			var website model.WebsiteConfig
			if err := json.Unmarshal(event.GetMsg(), &website); err != nil {
				return err
			}

			if err = generateConfigAndReload(&website); err != nil {
				logger.Error(err)
				if err := sendResponse(stream, EventTypeDeleteWebsite, []byte(err.Error())); err != nil {
					return err
				}
			} else {
				if err = sendResponse(stream, EventTypeDeleteWebsite, nil); err != nil {
					return err
				}
			}
		} else if event.Type == EventTypeFullWebsite {
			if err = generateFullConfigAndReload(event.Msg); err != nil {
				logger.Error(err)
				sendErr := sendResponse(stream, EventTypeFullWebsite, []byte(err.Error()))
				if sendErr != nil {
					return sendErr
				}
			} else {
				if err = sendResponse(stream, EventTypeFullWebsite, nil); err != nil {
					return err
				}
			}
		} else if event.Type == EventTypeDeleteWebsite {
			if err = deleteConfigAndReload(event.Msg); err != nil {
				logger.Error(err)
				sendErr := sendResponse(stream, EventTypeDeleteWebsite, []byte(err.Error()))
				if sendErr != nil {
					return sendErr
				}
			} else {
				if err = sendResponse(stream, EventTypeDeleteWebsite, nil); err != nil {
					return err
				}
			}
		}
	}
}
