package client

import (
	"archive/tar"
	"archive/zip"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"

	"repositories.action2quare.com/ayo/gocommon/logger"
	"repositories.action2quare.com/ayo/houston/shared"
	"repositories.action2quare.com/ayo/houston/shared/protos"

	"golang.org/x/text/encoding/korean"

	"golang.org/x/text/transform"
)

func pof2(x int64, min int64) (out int64) {
	out = 1
	org := x
	for (x >> 1) > 0 {
		out = out << 1
		x = x >> 1
	}
	if org > out {
		out = out << 1
	}

	if out < min {
		out = min
	}

	return
}

func download(dir string, urlpath string, accessToken string, cb func(int64, int64)) (target string, err error) {
	logger.Println("start downloading", dir, urlpath)
	defer func() {
		if err != nil {
			logger.Println("downloading failed :", err)
		} else {
			logger.Println("downloading succeeded")
		}
	}()

	parsed, err := url.Parse(urlpath)
	if err != nil {
		return "", err
	}

	req, _ := http.NewRequest("GET", urlpath, nil)
	req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.51")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("download failed : %d %s", resp.StatusCode, parsed.Path)
	}

	out, err := os.Create(path.Join(dir, path.Base(parsed.Path)))
	if err != nil {
		return "", err
	}
	defer out.Close()

	cl := resp.Header.Get("Content-Length")
	totalLength, _ := strconv.ParseInt(cl, 10, 0)
	totalWritten := int64(0)
	chunkSize := pof2(totalLength/100, 1024*1024)
	if cb != nil {
		cb(0, totalLength)
	}

	for {
		written, err := io.CopyN(out, resp.Body, chunkSize)
		totalWritten += written
		if cb != nil {
			cb(totalWritten, totalLength)
		}

		if err != nil {
			if err == io.EOF {
				break
			}
			return "", err
		}
	}

	return filepath.ToSlash(out.Name()), nil
}

func unzip(fname string) error {
	archive, err := zip.OpenReader(fname)
	if err != nil {
		os.Remove(fname)
		return err
	}
	defer func() {
		archive.Close()
		os.Remove(fname)
	}()

	verpath := path.Dir(fname)
	for _, f := range archive.File {
		var name string
		if f.NonUTF8 {
			name, _, _ = transform.String(korean.EUCKR.NewDecoder(), f.Name)
		} else {
			name = f.Name
		}

		name = strings.ReplaceAll(name, `\`, "/")
		filePath := path.Join(verpath, name)
		if f.FileInfo().IsDir() || strings.HasSuffix(f.FileInfo().Name(), `\`) {
			if err = os.MkdirAll(filePath, 0775); err != nil {
				return err
			}
			continue
		}

		if err := os.MkdirAll(path.Dir(filePath), 0775); err != nil {
			return err
		}

		dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
		if err != nil {
			return err
		}

		fileInArchive, err := f.Open()
		if err != nil {
			return err
		}

		if _, err := io.Copy(dstFile, fileInArchive); err != nil {
			return err
		}

		dstFile.Close()
		fileInArchive.Close()
	}

	return nil
}

func untar(fname string) error {
	file, err := os.Open(fname)
	if err != nil {
		return err
	}
	defer func() {
		file.Close()
		os.Remove(fname)
	}()

	verpath := path.Dir(fname)
	tarReader := tar.NewReader(file)
	for {
		header, err := tarReader.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}

		switch header.Typeflag {
		case tar.TypeDir:
			if err := os.MkdirAll(path.Join(verpath, header.Name), 0775); err != nil {
				return err
			}
		case tar.TypeReg:
			fileWriter, err := os.Create(path.Join(verpath, header.Name))
			if err != nil {
				return err
			}
			defer fileWriter.Close()

			if _, err := io.Copy(fileWriter, tarReader); err != nil {
				return err
			}
		default:
			return errors.New("unknown type")
		}
	}

	return nil
}

func (hc *houstonClient) prepareDeploy(name string, version string) (destPath string, err error) {
	// houston관리용임을 표시하기 위해 더미파일 생성
	defer func() {
		var flagf *os.File
		markerPath := path.Join(hc.config.StorageRoot, name, "@houston")
		if _, err := os.Stat(markerPath); os.IsNotExist(err) {
			flagf, err = os.Create(markerPath)
			if err != nil {
				return
			}
			defer flagf.Close()
			flagf.Write([]byte(hc.timestamp))
		}
	}()

	verpath := path.Join(hc.config.StorageRoot, name, version)
	if _, err := os.Stat(verpath); os.IsNotExist(err) {
		// 없네? 만들면 된다.
		err = os.MkdirAll(verpath, 0775)
		if err != nil {
			return "", err
		}
	} else {
		// 있네? 재배포 가능한가?
		for _, child := range hc.childProcs {
			if child.version == version && child.name == name {
				// 이미 실행 중인 버전이다. 실패
				return "", fmt.Errorf("%s %s is already running. deploy is failed", name, version)
			}
		}
		// 재배포 가능
	}
	return verpath, nil
}

func (hc *houstonClient) makeDownloadUrl(rel string) string {
	out := rel
	if !strings.HasPrefix(out, "http") {
		tks := strings.SplitN(hc.config.HttpAddress, "://", 2)
		out = fmt.Sprintf("%s://%s", tks[0], path.Join(tks[1], rel))
	}
	return out
}

func copy(src, dst string) error {
	fi, err := os.Stat(src)
	if err != nil {
		return err
	}
	inmode := fi.Mode()

	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()

	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()

	copied, err := io.Copy(out, in)
	if err != nil {
		return err
	}
	if copied < fi.Size() {
		return errors.New("copy not completed")
	}
	if err := out.Sync(); err != nil {
		return err
	}

	if err := out.Chmod(inmode); err != nil {
		return err
	}

	return nil
}

func (hc *houstonClient) prepareUpdateSelf(req *shared.DeployRequest) (srcdir string, replacer string, err error) {
	// 내가 스스로 업데이트
	// 다운로드 받고 압축 푼 다음에 교체용 프로세스 시작
	tempdir, err := os.MkdirTemp(os.TempDir(), "*")
	if err != nil {
		return "", "", err
	}
	fname, err := download(tempdir, hc.makeDownloadUrl(req.Url), req.AccessToken, nil)
	if err != nil {
		return "", "", err
	}

	switch path.Ext(fname) {
	case ".zip":
		err = unzip(fname)
	case ".tar":
		err = untar(fname)
	}

	if err != nil {
		return "", "", err
	}

	// houston version 파일
	err = os.WriteFile(path.Join(path.Dir(fname), "@version"), []byte(req.Version), 0644)
	if err != nil {
		return "", "", err
	}

	selfname, _ := os.Executable()
	srcreplacer := path.Join(path.Dir(fname), "replacer") + path.Ext(selfname)
	replacer = "./" + filepath.ToSlash("replacer"+path.Ext(selfname))
	err = copy(srcreplacer, replacer)
	if err == nil {
		err = os.Chmod(replacer, 0775)
	}

	// replacer먼저 가져옴
	return filepath.ToSlash(tempdir), replacer, err
}

func (hc *houstonClient) deploy(req *shared.DeployRequest, cb func(*protos.DeployingProgress)) error {
	logger.Println("start deploying")
	root, err := hc.prepareDeploy(req.Name, req.Version)
	if err != nil {
		return err
	}

	// verpath에 배포 시작
	fname, err := download(root, hc.makeDownloadUrl(req.Url), req.AccessToken, func(written int64, total int64) {
		prog := protos.DeployingProgress{
			State:    "download",
			Progress: written,
			Total:    total,
		}
		cb(&prog)
	})

	if err != nil {
		return err
	}

	cb(&protos.DeployingProgress{
		State:    "unpack",
		Progress: 0,
		Total:    0,
	})

	switch path.Ext(fname) {
	case ".zip":
		err = unzip(fname)
	case ".tar":
		err = untar(fname)
	}

	if err == nil && len(req.Config) > 0 {
		// config.json도 다운로드
		_, err = download(root, hc.makeDownloadUrl(req.Config), req.AccessToken, nil)
	}

	return err
}

func (hc *houstonClient) withdraw(req *shared.WithdrawRequest) error {
	targetPath := path.Join(hc.config.StorageRoot, req.Name, req.Version)
	fd, _ := os.Stat(targetPath)
	if fd != nil {
		if fd.IsDir() {
			for _, running := range hc.childProcs {
				if running.name == req.Name && (len(req.Version) == 0 || running.version == req.Version) {
					// 회수하려는 버전이 돌고 있다
					return fmt.Errorf("withdraw failed. %s@%s is still running", req.Name, req.Version)
				}
			}

			return os.RemoveAll(targetPath)
		}
	}

	return fmt.Errorf("withdraw failed. %s@%s is not deployed", req.Name, req.Version)
}
