package service

import (
	"bufio"
	"context"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path"
	"reflect"
	"strings"
	"time"

	"github.com/1Panel-dev/1Panel/backend/app/dto"
	"github.com/1Panel-dev/1Panel/backend/app/model"
	"github.com/1Panel-dev/1Panel/backend/constant"
	"github.com/1Panel-dev/1Panel/backend/global"
	"github.com/1Panel-dev/1Panel/backend/utils/compose"
	"github.com/1Panel-dev/1Panel/backend/utils/files"
	"github.com/pkg/errors"
	"gorm.io/gorm"
)

type WebsiteService struct {
}

type IWebsiteService interface {
	PageWebSite(req dto.WebSiteReq) (int64, []dto.WebSiteDTO, error)
	CreateWebsite(create dto.WebSiteCreate) error
	GetWebsiteOptions() ([]string, error)
	Backup(domain string) error
	Recover(req dto.WebSiteRecover) error
	RecoverByUpload(req dto.WebSiteRecoverByFile) error
	UpdateWebsite(req dto.WebSiteUpdate) error
	DeleteWebSite(req dto.WebSiteDel) error
}

func NewWebsiteService() IWebsiteService {
	return &WebsiteService{}
}

func (w WebsiteService) PageWebSite(req dto.WebSiteReq) (int64, []dto.WebSiteDTO, error) {
	var websiteDTOs []dto.WebSiteDTO
	total, websites, err := websiteRepo.Page(req.Page, req.PageSize)
	if err != nil {
		return 0, nil, err
	}
	for _, web := range websites {
		websiteDTOs = append(websiteDTOs, dto.WebSiteDTO{
			WebSite: web,
		})
	}
	return total, websiteDTOs, nil
}

func (w WebsiteService) CreateWebsite(create dto.WebSiteCreate) error {

	defaultDate, _ := time.Parse(constant.DateLayout, constant.DefaultDate)
	website := &model.WebSite{
		PrimaryDomain:  create.PrimaryDomain,
		Type:           create.Type,
		Alias:          create.Alias,
		Remark:         create.Remark,
		Status:         constant.WebRunning,
		ExpireDate:     defaultDate,
		AppInstallID:   create.AppInstallID,
		WebSiteGroupID: create.WebSiteGroupID,
		Protocol:       constant.ProtocolHTTP,
	}

	if create.Type == "deployment" {
		if create.AppType == dto.NewApp {
			install, err := ServiceGroupApp.Install(create.AppInstall.Name, create.AppInstall.AppDetailId, create.AppInstall.Params)
			if err != nil {
				return err
			}
			website.AppInstallID = install.ID
		}
	} else {
		if err := createStaticHtml(website); err != nil {
			return err
		}
	}

	tx, ctx := getTxAndContext()
	if err := websiteRepo.Create(ctx, website); err != nil {
		return err
	}
	var domains []model.WebSiteDomain
	domains = append(domains, model.WebSiteDomain{Domain: website.PrimaryDomain, WebSiteID: website.ID, Port: 80})

	otherDomainArray := strings.Split(create.OtherDomains, "\n")
	for _, domain := range otherDomainArray {
		if domain == "" {
			continue
		}
		domainModel, err := getDomain(domain, website.ID)
		if err != nil {
			tx.Rollback()
			return err
		}
		if reflect.DeepEqual(domainModel, model.WebSiteDomain{}) {
			continue
		}
		domains = append(domains, domainModel)
	}
	if len(domains) > 0 {
		if err := websiteDomainRepo.BatchCreate(ctx, domains); err != nil {
			tx.Rollback()
			return err
		}
	}

	if err := configDefaultNginx(website, domains); err != nil {
		tx.Rollback()
		return err
	}

	tx.Commit()
	return nil
}

func (w WebsiteService) GetWebsiteOptions() ([]string, error) {
	webs, err := websiteRepo.GetBy()
	if err != nil {
		return nil, err
	}
	var datas []string
	for _, web := range webs {
		datas = append(datas, web.PrimaryDomain)
	}
	return datas, nil
}

func (w WebsiteService) Backup(domain string) error {
	localDir, err := loadLocalDir()
	if err != nil {
		return err
	}
	fileName := fmt.Sprintf("%s_%s", domain, time.Now().Format("20060102150405"))
	backupDir := fmt.Sprintf("website/%s", domain)

	if err := handleWebsiteBackup("LOCAL", localDir, backupDir, domain, fileName); err != nil {
		return err
	}
	return nil
}

func (w WebsiteService) RecoverByUpload(req dto.WebSiteRecoverByFile) error {
	if err := handleUnTar(fmt.Sprintf("%s/%s", req.FileDir, req.FileName), req.FileDir); err != nil {
		return err
	}
	tmpDir := fmt.Sprintf("%s/%s", req.FileDir, strings.ReplaceAll(req.FileName, ".tar.gz", ""))
	webJson, err := os.ReadFile(fmt.Sprintf("%s/website.json", tmpDir))
	if err != nil {
		return err
	}
	var websiteInfo WebSiteInfo
	if err := json.Unmarshal(webJson, &websiteInfo); err != nil {
		return err
	}
	if websiteInfo.WebsiteName != req.WebsiteName || websiteInfo.WebsiteType != req.Type {
		return errors.New("上传文件与选中网站不匹配，无法恢复")
	}

	website, err := websiteRepo.GetFirst(websiteRepo.WithByDomain(req.WebsiteName))
	if err != nil {
		return err
	}
	if err := handleWebsiteRecover(&website, tmpDir); err != nil {
		return err
	}

	return nil
}

func (w WebsiteService) Recover(req dto.WebSiteRecover) error {
	website, err := websiteRepo.GetFirst(websiteRepo.WithByDomain(req.WebsiteName))
	if err != nil {
		return err
	}

	if !strings.Contains(req.BackupName, "/") {
		return errors.New("error path of request")
	}
	fileDir := req.BackupName[:strings.LastIndex(req.BackupName, "/")]
	fileName := strings.ReplaceAll(req.BackupName[strings.LastIndex(req.BackupName, "/"):], ".tar.gz", "")
	if err := handleUnTar(req.BackupName, fileDir); err != nil {
		return err
	}
	fileDir = fileDir + "/" + fileName

	if err := handleWebsiteRecover(&website, fileDir); err != nil {
		return err
	}
	return nil
}

func (w WebsiteService) UpdateWebsite(req dto.WebSiteUpdate) error {
	website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID))
	if err != nil {
		return err
	}
	website.PrimaryDomain = req.PrimaryDomain
	website.WebSiteGroupID = req.WebSiteGroupID
	website.Remark = req.Remark

	return websiteRepo.Save(context.TODO(), &website)
}

func (w WebsiteService) GetWebsite(id uint) (dto.WebsiteDTO, error) {
	var res dto.WebsiteDTO
	website, err := websiteRepo.GetFirst(commonRepo.WithByID(id))
	if err != nil {
		return res, err
	}
	res.WebSite = website
	return res, nil
}

func (w WebsiteService) DeleteWebSite(req dto.WebSiteDel) error {

	website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID))
	if err != nil {
		return err
	}
	if err := delNginxConfig(website); err != nil {
		return err
	}
	tx, ctx := getTxAndContext()

	if req.DeleteApp {
		websites, _ := websiteRepo.GetBy(websiteRepo.WithAppInstallId(website.AppInstallID))
		if len(websites) > 1 {
			return errors.New("other website use this app")
		}
		appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID))
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return err
		}
		if !reflect.DeepEqual(model.AppInstall{}, appInstall) {
			if err := deleteAppInstall(ctx, appInstall); err != nil {
				return err
			}
		}
	}
	//TODO 删除备份
	if err := websiteRepo.DeleteBy(ctx, commonRepo.WithByID(req.ID)); err != nil {
		tx.Rollback()
		return err
	}
	if err := websiteDomainRepo.DeleteBy(ctx, websiteDomainRepo.WithWebSiteId(req.ID)); err != nil {
		tx.Rollback()
		return err
	}
	tx.Commit()
	return nil
}

func (w WebsiteService) CreateWebsiteDomain(create dto.WebSiteDomainCreate) (model.WebSiteDomain, error) {
	var domainModel model.WebSiteDomain
	var ports []int
	var domains []string

	website, err := websiteRepo.GetFirst(commonRepo.WithByID(create.WebSiteID))
	if err != nil {
		return domainModel, err
	}
	if oldDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebSiteId(create.WebSiteID), websiteDomainRepo.WithPort(create.Port)); len(oldDomains) == 0 {
		ports = append(ports, create.Port)
	}
	domains = append(domains, create.Domain)
	if err := addListenAndServerName(website, ports, domains); err != nil {
		return domainModel, err
	}
	domainModel = model.WebSiteDomain{
		Domain:    create.Domain,
		Port:      create.Port,
		WebSiteID: create.WebSiteID,
	}
	return domainModel, websiteDomainRepo.Create(context.TODO(), &domainModel)
}

func (w WebsiteService) GetWebsiteDomain(websiteId uint) ([]model.WebSiteDomain, error) {
	return websiteDomainRepo.GetBy(websiteDomainRepo.WithWebSiteId(websiteId))
}

func (w WebsiteService) DeleteWebsiteDomain(domainId uint) error {

	webSiteDomain, err := websiteDomainRepo.GetFirst(commonRepo.WithByID(domainId))
	if err != nil {
		return err
	}

	if websiteDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebSiteId(webSiteDomain.WebSiteID)); len(websiteDomains) == 1 {
		return fmt.Errorf("can not delete last domain")
	}
	website, err := websiteRepo.GetFirst(commonRepo.WithByID(webSiteDomain.WebSiteID))
	if err != nil {
		return err
	}
	var ports []int
	if oldDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebSiteId(webSiteDomain.WebSiteID), websiteDomainRepo.WithPort(webSiteDomain.Port)); len(oldDomains) == 1 {
		ports = append(ports, webSiteDomain.Port)
	}

	var domains []string
	if oldDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebSiteId(webSiteDomain.WebSiteID), websiteDomainRepo.WithDomain(webSiteDomain.Domain)); len(oldDomains) == 1 {
		domains = append(domains, webSiteDomain.Domain)
	}
	if len(ports) > 0 || len(domains) > 0 {
		if err := deleteListenAndServerName(website, ports, domains); err != nil {
			return err
		}
	}

	return websiteDomainRepo.DeleteBy(context.TODO(), commonRepo.WithByID(domainId))
}

func (w WebsiteService) GetNginxConfigByScope(req dto.NginxConfigReq) ([]dto.NginxParam, error) {

	keys, ok := dto.ScopeKeyMap[req.Scope]
	if !ok || len(keys) == 0 {
		return nil, nil
	}

	website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebSiteID))
	if err != nil {
		return nil, err
	}

	return getNginxConfigByKeys(website, keys)
}

func (w WebsiteService) UpdateNginxConfigByScope(req dto.NginxConfigReq) error {

	keys, ok := dto.ScopeKeyMap[req.Scope]
	if !ok || len(keys) == 0 {
		return nil
	}
	website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebSiteID))
	if err != nil {
		return err
	}
	if req.Operate == dto.ConfigDel {
		return deleteNginxConfig(website, keys)
	}

	return updateNginxConfig(website, getNginxParams(req.Params, keys), req.Scope)
}

func (w WebsiteService) GetWebsiteNginxConfig(websiteId uint) (dto.FileInfo, error) {
	website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteId))
	if err != nil {
		return dto.FileInfo{}, err
	}

	nginxApp, err := appRepo.GetFirst(appRepo.WithKey("nginx"))
	if err != nil {
		return dto.FileInfo{}, err
	}
	nginxInstall, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(nginxApp.ID))
	if err != nil {
		return dto.FileInfo{}, err
	}

	configPath := path.Join(constant.AppInstallDir, "nginx", nginxInstall.Name, "conf", "conf.d", website.Alias+".conf")

	info, err := files.NewFileInfo(files.FileOption{
		Path:   configPath,
		Expand: true,
	})
	if err != nil {
		return dto.FileInfo{}, err
	}
	return dto.FileInfo{FileInfo: *info}, nil
}

func (w WebsiteService) GetWebsiteHTTPS(websiteId uint) (dto.WebsiteHTTPS, error) {
	website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteId))
	if err != nil {
		return dto.WebsiteHTTPS{}, err
	}
	var res dto.WebsiteHTTPS
	if website.WebSiteSSLID == 0 {
		res.Enable = false
		return res, nil
	}
	websiteSSL, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(website.WebSiteSSLID))
	if err != nil {
		return dto.WebsiteHTTPS{}, err
	}
	res.SSL = websiteSSL
	res.Enable = true
	return res, nil
}

func (w WebsiteService) OpWebsiteHTTPS(req dto.WebsiteHTTPSOp) (dto.WebsiteHTTPS, error) {
	website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
	if err != nil {
		return dto.WebsiteHTTPS{}, err
	}

	var (
		res        dto.WebsiteHTTPS
		websiteSSL model.WebSiteSSL
	)
	res.Enable = req.Enable

	if req.Type == dto.SSLExisted {
		websiteSSL, err = websiteSSLRepo.GetFirst(commonRepo.WithByID(req.WebsiteSSLID))
		if err != nil {
			return dto.WebsiteHTTPS{}, err
		}
		website.WebSiteSSLID = websiteSSL.ID
		if err := websiteRepo.Save(context.TODO(), &website); err != nil {
			return dto.WebsiteHTTPS{}, err
		}
		res.SSL = websiteSSL
	}

	if req.Type == dto.Manual {
		certBlock, _ := pem.Decode([]byte(req.Certificate))
		cert, err := x509.ParseCertificate(certBlock.Bytes)
		if err != nil {
			return dto.WebsiteHTTPS{}, err
		}
		websiteSSL.ExpireDate = cert.NotAfter
		websiteSSL.StartDate = cert.NotBefore
		websiteSSL.Type = cert.Issuer.CommonName
		websiteSSL.Organization = cert.Issuer.Organization[0]
		websiteSSL.PrimaryDomain = cert.Subject.CommonName
		if len(cert.Subject.Names) > 0 {
			var domains []string
			for _, name := range cert.Subject.Names {
				if v, ok := name.Value.(string); ok {
					if v != cert.Subject.CommonName {
						domains = append(domains, v)
					}
				}
			}
			if len(domains) > 0 {
				websiteSSL.Domains = strings.Join(domains, "")
			}
		}

		websiteSSL.Provider = dto.Manual
		websiteSSL.PrivateKey = req.PrivateKey
		websiteSSL.Pem = req.Certificate

		res.SSL = websiteSSL
	}

	if req.Enable {
		website.Protocol = constant.ProtocolHTTPS
		if err := applySSL(website, websiteSSL); err != nil {
			return dto.WebsiteHTTPS{}, err
		}
	} else {
		website.Protocol = constant.ProtocolHTTP
		website.WebSiteSSLID = 0

		if err := deleteListenAndServerName(website, []int{443}, []string{}); err != nil {
			return dto.WebsiteHTTPS{}, err
		}

		if err := deleteNginxConfig(website, getKeysFromStaticFile(dto.SSL)); err != nil {
			return dto.WebsiteHTTPS{}, err
		}
	}

	tx, ctx := getTxAndContext()
	if websiteSSL.ID == 0 {
		if err := websiteSSLRepo.Create(ctx, &websiteSSL); err != nil {
			return dto.WebsiteHTTPS{}, err
		}
		website.WebSiteSSLID = websiteSSL.ID
	}
	if err := websiteRepo.Save(ctx, &website); err != nil {
		return dto.WebsiteHTTPS{}, err
	}

	tx.Commit()
	return res, nil
}

type WebSiteInfo struct {
	WebsiteName string `json:"websiteName"`
	WebsiteType string `json:"websiteType"`
}

func handleWebsiteBackup(backupType, baseDir, backupDir, domain, backupName string) error {
	website, err := websiteRepo.GetFirst(websiteRepo.WithByDomain(domain))
	if err != nil {
		return err
	}

	tmpDir := fmt.Sprintf("%s/%s/%s", baseDir, backupDir, backupName)
	if _, err := os.Stat(tmpDir); err != nil && os.IsNotExist(err) {
		if err = os.MkdirAll(tmpDir, os.ModePerm); err != nil {
			if err != nil {
				return fmt.Errorf("mkdir %s failed, err: %v", tmpDir, err)
			}
		}
	}
	if err := saveWebsiteJson(&website, tmpDir); err != nil {
		return err
	}

	nginxInfo, err := appInstallRepo.LoadBaseInfoByKey("nginx")
	if err != nil {
		return err
	}
	nginxConfFile := fmt.Sprintf("%s/nginx/%s/conf/conf.d/%s.conf", constant.AppInstallDir, nginxInfo.Name, website.PrimaryDomain)
	if err := copyConf(nginxConfFile, fmt.Sprintf("%s/%s.conf", tmpDir, website.PrimaryDomain)); err != nil {
		return err
	}

	if website.Type == "deployment" {
		if err := mysqlOpration(&website, "backup", tmpDir); err != nil {
			return err
		}

		app, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID))
		if err != nil {
			return err
		}
		websiteDir := fmt.Sprintf("%s/%s/%s", constant.AppInstallDir, app.App.Key, app.Name)
		if err := handleTar(websiteDir, tmpDir, fmt.Sprintf("%s.web.tar.gz", website.PrimaryDomain), ""); err != nil {
			return err
		}
	} else {
		websiteDir := fmt.Sprintf("%s/nginx/%s/www/%s", constant.AppInstallDir, nginxInfo.Name, website.PrimaryDomain)
		if err := handleTar(websiteDir, tmpDir, fmt.Sprintf("%s.web.tar.gz", website.PrimaryDomain), ""); err != nil {
			return err
		}
	}
	if err := handleTar(tmpDir, fmt.Sprintf("%s/%s", baseDir, backupDir), backupName+".tar.gz", ""); err != nil {
		return err
	}
	_ = os.RemoveAll(tmpDir)

	record := &model.BackupRecord{
		Type:       "website-" + website.Type,
		Name:       website.PrimaryDomain,
		DetailName: "",
		Source:     backupType,
		BackupType: backupType,
		FileDir:    backupDir,
		FileName:   fmt.Sprintf("%s.tar.gz", backupName),
	}
	if baseDir != constant.TmpDir || backupType == "LOCAL" {
		record.Source = "LOCAL"
		record.FileDir = fmt.Sprintf("%s/%s", baseDir, backupDir)
	}
	if err := backupRepo.CreateRecord(record); err != nil {
		global.LOG.Errorf("save backup record failed, err: %v", err)
	}
	return nil
}

func handleWebsiteRecover(website *model.WebSite, fileDir string) error {
	nginxInfo, err := appInstallRepo.LoadBaseInfoByKey("nginx")
	if err != nil {
		return err
	}
	nginxConfFile := fmt.Sprintf("%s/nginx/%s/conf/conf.d/%s.conf", constant.AppInstallDir, nginxInfo.Name, website.PrimaryDomain)
	if err := copyConf(fmt.Sprintf("%s/%s.conf", fileDir, website.PrimaryDomain), nginxConfFile); err != nil {
		return err
	}

	if website.Type == "deployment" {
		if err := mysqlOpration(website, "recover", fileDir); err != nil {
			return err
		}

		app, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID))
		if err != nil {
			return err
		}
		appDir := fmt.Sprintf("%s/%s", constant.AppInstallDir, app.App.Key)
		if err := handleUnTar(fmt.Sprintf("%s/%s.web.tar.gz", fileDir, website.PrimaryDomain), appDir); err != nil {
			return err
		}
		if _, err := compose.Restart(fmt.Sprintf("%s/%s/docker-compose.yml", appDir, app.Name)); err != nil {
			return err
		}
	} else {
		appDir := fmt.Sprintf("%s/nginx/%s/www", constant.AppInstallDir, nginxInfo.Name)
		if err := handleUnTar(fmt.Sprintf("%s/%s.web.tar.gz", fileDir, website.PrimaryDomain), appDir); err != nil {
			return err
		}
	}
	cmd := exec.Command("docker", "exec", "-i", nginxInfo.ContainerName, "nginx", "-s", "reload")
	stdout, err := cmd.CombinedOutput()
	if err != nil {
		return errors.New(string(stdout))
	}
	_ = os.RemoveAll(fileDir)

	return nil
}

func mysqlOpration(website *model.WebSite, operation, filePath string) error {
	mysqlInfo, err := appInstallRepo.LoadBaseInfoByKey("mysql")
	if err != nil {
		return err
	}
	resource, err := appInstallResourceRepo.GetFirst(appInstallResourceRepo.WithAppInstallId(website.AppInstallID))
	if err != nil {
		return err
	}
	db, err := mysqlRepo.Get(commonRepo.WithByID(resource.ResourceId))
	if err != nil {
		return err
	}
	if operation == "backup" {
		dbFile := fmt.Sprintf("%s/%s.sql", filePath, website.PrimaryDomain)
		outfile, _ := os.OpenFile(dbFile, os.O_RDWR|os.O_CREATE, 0755)
		defer outfile.Close()
		cmd := exec.Command("docker", "exec", mysqlInfo.ContainerName, "mysqldump", "-uroot", "-p"+mysqlInfo.Password, db.Name)
		cmd.Stdout = outfile
		_ = cmd.Run()
		_ = cmd.Wait()
		return nil
	}
	cmd := exec.Command("docker", "exec", "-i", mysqlInfo.ContainerName, "mysql", "-uroot", "-p"+mysqlInfo.Password, db.Name)
	sqlfile, err := os.Open(fmt.Sprintf("%s/%s.sql", filePath, website.PrimaryDomain))
	if err != nil {
		return err
	}
	defer sqlfile.Close()
	cmd.Stdin = sqlfile
	stdout, err := cmd.CombinedOutput()
	if err != nil {
		return errors.New(string(stdout))
	}
	return nil
}

func saveWebsiteJson(website *model.WebSite, tmpDir string) error {
	var WebSiteInfo WebSiteInfo
	WebSiteInfo.WebsiteType = website.Type
	WebSiteInfo.WebsiteName = website.PrimaryDomain
	remarkInfo, _ := json.Marshal(WebSiteInfo)
	path := fmt.Sprintf("%s/website.json", tmpDir)
	file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
	if err != nil {
		return err
	}
	defer file.Close()
	write := bufio.NewWriter(file)
	_, _ = write.WriteString(string(remarkInfo))
	write.Flush()
	return nil
}

func copyConf(srcPath, dstPath string) error {
	if _, err := os.Stat(srcPath); err != nil {
		return err
	}
	src, err := os.OpenFile(srcPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
	if err != nil {
		return err
	}
	defer src.Close()
	out, err := os.Create(dstPath)
	if err != nil {
		return err
	}
	defer out.Close()
	_, _ = io.Copy(out, src)
	return nil
}
