package microsite

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"path"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/hashicorp/go-multierror"
	archiver "github.com/mholt/archiver/v4"
	"github.com/qor/oss"
	"github.com/qor5/admin/microsite/utils"
	"github.com/qor5/admin/publish"
	"gorm.io/gorm"
)

type MicroSite struct {
	gorm.Model
	publish.Status
	publish.Schedule
	publish.Version

	PrePath string

	Package   FileSystem `gorm:"type:text"`
	FilesList string     `gorm:"type:text"`

	UnixKey string
}

func (this *MicroSite) PermissionRN() []string {
	return []string{"microsite_models", strconv.Itoa(int(this.ID)), this.Version.Version}
}

func (this *MicroSite) PrimarySlug() string {
	return fmt.Sprintf("%v_%v", this.ID, this.Version.Version)
}

func (this *MicroSite) PrimaryColumnValuesBySlug(slug string) map[string]string {
	segs := strings.Split(slug, "_")
	if len(segs) != 2 {
		panic("wrong slug")
	}

	return map[string]string{
		"id":      segs[0],
		"version": segs[1],
	}
}

func (this MicroSite) GetID() uint {
	return this.ID
}

func (this MicroSite) GetUnixKey() string {
	return this.UnixKey
}

func (this *MicroSite) SetUnixKey() {
	this.UnixKey = strconv.FormatInt(time.Now().UnixMilli(), 10)
	return
}

func (this MicroSite) GetPackagePath(fileName string) string {
	return strings.TrimPrefix(path.Join(PackageAndPreviewPrepath, "__package__", this.GetUnixKey(), fileName), "/")
}

func (this MicroSite) GetPreviewPath(fileName string) string {
	return strings.TrimPrefix(path.Join(PackageAndPreviewPrepath, "__preview__", this.GetUnixKey(), fileName), "/")
}

func (this MicroSite) GetPreviewUrl(domain, fileName string) string {
	return strings.TrimSuffix(domain, "/") + "/" + this.GetPreviewPath(fileName)
}

func (this MicroSite) GetPublishedPath(fileName string) string {
	return path.Join(strings.TrimPrefix(strings.TrimSuffix(this.PrePath, "/"), "/"), fileName)
}

func (this MicroSite) GetPublishedUrl(domain, fileName string) string {
	return strings.TrimSuffix(domain, "/") + "/" + this.GetPublishedPath(fileName)
}

func (this MicroSite) GetPackageUrl(domain string) string {
	return strings.TrimSuffix(domain, "/") + "/" + strings.TrimPrefix(this.Package.Url, "/")
}

func (this MicroSite) GetFileList() (arr []string) {
	json.Unmarshal([]byte(this.FilesList), &arr)
	return
}

func (this *MicroSite) SetFilesList(filesList []string) {
	list, err := json.Marshal(filesList)
	if err != nil {
		return
	}
	this.FilesList = string(list)
	return
}

func (this *MicroSite) GetPackage() FileSystem {
	return this.Package
}

func (this *MicroSite) SetPackage(fileName, url string) {
	this.Package.FileName = fileName
	this.Package.Url = url
	return
}

func (this *MicroSite) GetPublishActions(db *gorm.DB, ctx context.Context, storage oss.StorageInterface) (objs []*publish.PublishAction, err error) {
	if len(this.GetFileList()) > 0 {
		var previewPaths []string

		var wg = sync.WaitGroup{}
		var copyError error
		var mutex sync.Mutex
		for _, v := range this.GetFileList() {
			wg.Add(1)
			copySemaphore <- struct{}{}
			go func(v string) {
				defer func() {
					wg.Done()
					<-copySemaphore
				}()
				err = utils.Copy(storage, this.GetPreviewPath(v), this.GetPublishedPath(v))
				if err != nil {
					mutex.Lock()
					copyError = multierror.Append(copyError, err).ErrorOrNil()
					mutex.Unlock()
					return
				}
				mutex.Lock()
				previewPaths = append(previewPaths, this.GetPreviewPath(v))
				mutex.Unlock()
			}(v)
		}

		wg.Wait()

		if len(previewPaths) > 0 {
			err = utils.DeleteObjects(storage, previewPaths)
		}
		err = multierror.Append(err, copyError).ErrorOrNil()
	}
	return
}

func (this *MicroSite) GetUnPublishActions(db *gorm.DB, ctx context.Context, storage oss.StorageInterface) (objs []*publish.PublishAction, err error) {
	var paths []string
	for _, v := range this.GetFileList() {
		paths = append(paths, this.GetPublishedPath(v))
	}
	err = utils.DeleteObjects(storage, paths)
	if err != nil {
		return
	}
	return
}

func (this *MicroSite) UnArchiveAndPublish(getPath func(string) string, fileName string, f io.Reader, storage oss.StorageInterface) (filesList []string, err error) {
	format, reader, err := archiver.Identify(fileName, f)
	if err != nil {
		if err == archiver.ErrNoMatch {
			err = utils.Upload(storage, getPath(fileName), f)
			return
		}
		return
	}

	var wg = sync.WaitGroup{}
	var putError error
	var mutex sync.Mutex

	err = format.(archiver.Extractor).Extract(context.Background(), reader, nil, func(ctx context.Context, f archiver.File) (err error) {
		if f.IsDir() {
			return
		}

		rc, err := f.Open()
		if err != nil {
			return
		}
		defer rc.Close()

		filesList = append(filesList, f.NameInArchive)

		publishedPath := getPath(f.NameInArchive)
		wg.Add(1)
		putSemaphore <- struct{}{}
		go func() {
			defer func() {
				<-putSemaphore
				wg.Done()
			}()
			err2 := utils.Upload(storage, publishedPath, rc)
			if err2 != nil {
				mutex.Lock()
				putError = multierror.Append(putError, err2).ErrorOrNil()
				mutex.Unlock()
			}
		}()

		return
	})
	wg.Wait()
	err = multierror.Append(err, putError).ErrorOrNil()
	return
}