package update

import (
	"archive/zip"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"github.com/bestruirui/bestsub/internal/core/mihomo"
	"github.com/bestruirui/bestsub/internal/database/op"
	"github.com/bestruirui/bestsub/internal/models/setting"
	"github.com/bestruirui/bestsub/internal/utils/log"
)

const (
	bestsubUpdateUrl    = "https://github.com/bestruirui/bestsub/releases/latest/download"
	bestsubUpdateApiUrl = "https://api.github.com/repos/bestruirui/BestSub/releases/latest"
	frontUpdateApiUrl   = "https://api.github.com/repos/bestruirui/BestSubFront/releases/latest"
	subcerUpdateApiUrl  = "https://api.github.com/repos/bestruirui/subconverter/releases/latest"
)

type LatestInfo struct {
	TagName     string `json:"tag_name"`
	PublishedAt string `json:"published_at"`
	Body        string `json:"body"`
	Message     string `json:"message"`
}

func GetLatestUIInfo() (*LatestInfo, error) {
	return getLatestInfo(frontUpdateApiUrl, op.GetSettingBool(setting.FRONTEND_URL_PROXY))
}

func GetLatestSubconverterInfo() (*LatestInfo, error) {
	return getLatestInfo(subcerUpdateApiUrl, op.GetSettingBool(setting.SUBCONVERTER_URL_PROXY))
}

func GetLatestBestsubInfo() (*LatestInfo, error) {
	return getLatestInfo(bestsubUpdateApiUrl, op.GetSettingBool(setting.PROXY_ENABLE))
}

func getLatestInfo(url string, proxy bool) (*LatestInfo, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	hc := mihomo.Default(proxy)
	if hc == nil {
		return nil, fmt.Errorf("failed to create http client")
	}
	defer hc.Release()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		log.Debugf("new request failed: %v", err)
		return nil, err
	}
	resp, err := hc.Do(req)
	if err != nil {
		log.Debugf("request failed: %v", err)
		return nil, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Debugf("read body failed: %v", err)
		return nil, err
	}

	latestInfo := LatestInfo{}
	err = json.Unmarshal(body, &latestInfo)
	if err != nil {
		log.Debugf("unmarshal body failed: %v", err)
		return nil, err
	}
	if latestInfo.Message != "" {
		return nil, fmt.Errorf("failed to get latest info: %s", latestInfo.Message)
	}
	return &latestInfo, nil
}

func download(url string, proxy bool) ([]byte, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	hc := mihomo.Default(proxy)
	if hc == nil {
		return nil, fmt.Errorf("failed to create http client")
	}
	defer hc.Release()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		log.Debugf("new request failed: %v", err)
		return nil, err
	}
	resp, err := hc.Do(req)
	if err != nil {
		log.Debugf("request failed: %v", err)
		return nil, err
	}
	defer resp.Body.Close()

	bytes, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Debugf("read body failed: %v", err)
		return nil, err
	}
	return bytes, nil
}

func unzip(data []byte, dest string) error {
	r, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
	if err != nil {
		log.Debugf("new zip reader failed: %v", err)
		return err
	}

	for _, f := range r.File {
		fpath := filepath.Join(dest, f.Name)

		if !inDest(fpath, dest) {
			log.Debugf("invalid file path: %s", fpath)
			return fmt.Errorf("invalid file path: %s", fpath)
		}
		info := f.FileInfo()
		if info.IsDir() {
			os.MkdirAll(fpath, os.ModePerm)
			continue
		}
		if info.Mode()&os.ModeSymlink != 0 {
			continue
		}
		if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
			log.Debugf("mkdir all failed: %v", err)
			return err
		}
		outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode().Perm())
		if err != nil {
			err = os.Remove(fpath)
			if err != nil {
				log.Debugf("remove file failed: %v", err)
				return err
			}
			outFile, err = os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
			if err != nil {
				log.Debugf("open file failed: %v", err)
				return err
			}
		}
		defer outFile.Close()
		rc, err := f.Open()
		if err != nil {
			log.Debugf("open file failed: %v", err)
			return err
		}
		_, err = io.Copy(outFile, rc)
		rc.Close()
		if err != nil {
			log.Debugf("copy failed: %v", err)
			return err
		}
	}
	return nil
}

func inDest(fpath, dest string) bool {
	if rel, err := filepath.Rel(dest, fpath); err == nil {
		if filepath.IsLocal(rel) {
			return true
		}
	}
	return false
}
