package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"time"
)

func authInfo(host string, port int, user, pswd string) bool {
	linkAuthInfo := fmt.Sprintf(
		"http://%s:%d/auth/info?user=%s&pswd=%v", host, port, user, pswd,
	)
	urlAuthInfo, err := url.Parse(linkAuthInfo)
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(3 * time.Second)
		return false
	}
	resp, err := http.Get(urlAuthInfo.String())
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(3 * time.Second)
		return false
	}
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(3 * time.Second)
		return false
	}
	var result map[string]any
	err = json.Unmarshal(body, &result)
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(3 * time.Second)
		return false
	}
	var resultAuthInfo map[string]any
	err = json.Unmarshal(body, &resultAuthInfo)
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(3 * time.Second)
		return false
	}
	return true
}

func authLogin(host string, port int, user, pswd string) bool {
	linkAuthLogin := fmt.Sprintf(
		"http://%s:%d/auth/login?user=%s&pswd=%v", host, port, user, pswd,
	)

	urlAuthLogin, err := url.Parse(linkAuthLogin)
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(10 * time.Second)
		return false
	}

	resp, err := http.Get(urlAuthLogin.String())
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(10 * time.Second)
		return false
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(10 * time.Second)
		return false
	}

	var result map[string]any
	err = json.Unmarshal(body, &result)
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(10 * time.Second)
		return false
	}

	success, ok := result["success"].(bool)
	if !ok || !success {
		fmt.Println("login failed, retrying...", result)
		time.Sleep(10 * time.Second)
		return false
	}

	fmt.Println("login successful")
	return true

}

type App struct {
	BundleID string `json:"bundleID"`
	Version  string `json:"version"`
}

func search(host string, port int, query string, limit int) ([]App, error) {
	linkSearch := fmt.Sprintf(
		"http://%s:%d/search?query=%s&limit=%d", host, port, query, limit,
	)
	urlSearch, err := url.Parse(linkSearch)
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(3 * time.Second)
		return nil, err
	}
	resp, err := http.Get(urlSearch.String())
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(3 * time.Second)
		return nil, err
	}
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(3 * time.Second)
		return nil, err
	}
	var resultSearch map[string]any
	err = json.Unmarshal(body, &resultSearch)
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(3 * time.Second)
		return nil, err
	}
	apps, ok := resultSearch["apps"].([]any)
	if !ok {
		return nil, fmt.Errorf("failed to get apps")
	}
	appsList := make([]App, len(apps))
	for i, app := range apps {
		app, ok := app.(map[string]any)
		if !ok {
			return nil, fmt.Errorf("failed to get app")
		}
		appsList[i] = App{
			BundleID: app["bundleID"].(string),
			Version:  app["version"].(string),
		}
	}
	return appsList, nil
}

type WriteCounter struct {
	Total      int64
	Downloaded int64
}

func (wc *WriteCounter) Write(p []byte) (int, error) {
	n := len(p)
	wc.Downloaded += int64(n)
	wc.PrintProgress()
	return n, nil
}

func (wc *WriteCounter) PrintProgress() {
	const MB = 1024 * 1024
	downloadedMB := float64(wc.Downloaded) / MB
	if wc.Total <= 0 {
		fmt.Printf("\rDownloading... %.2f MB", downloadedMB)
		return
	}
	totalMB := float64(wc.Total) / MB
	percentage := float64(wc.Downloaded) / float64(wc.Total) * 100
	fmt.Printf("\rDownloading... %.2f%% (%.2f/%.2f MB)", percentage, downloadedMB, totalMB)
}

func download(host string, port int, bundleID, query, version, udid, outputPath string) bool {
	linkDownload := fmt.Sprintf(
		"http://%s:%d/download?bundleID=%s", host, port, bundleID,
	)

	urlDownload, err := url.Parse(linkDownload)
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(3 * time.Second)
		return false
	}
	resp, err := http.Get(urlDownload.String())
	if err != nil {
		fmt.Println("error", err)
		time.Sleep(3 * time.Second)
		return false
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		fmt.Printf("error: server returned status %s\n", resp.Status)
		return false
	}

	fileName := filepath.Join(outputPath, fmt.Sprintf("%s_%s_%s.ipa", udid, query, version))
	out, err := os.Create(fileName)
	if err != nil {
		fmt.Println("error creating file:", err)
		return false
	}
	defer out.Close()

	counter := &WriteCounter{Total: resp.ContentLength}
	_, err = io.Copy(out, io.TeeReader(resp.Body, counter))
	if err != nil {
		fmt.Println("\nerror downloading:", err)
		return false
	}

	fmt.Print("\n") // New line after download completion
	return true
}

func checkVersionInDownloaded(udid, query, version, outputPath string) bool {
	path := filepath.Join(outputPath, fmt.Sprintf("%s_%s_%s.ipa", udid, query, version))
	_, err := os.Stat(path)
	return !os.IsNotExist(err)
}

func main() {

	bundleID := flag.String("b", "com.zhiliaoapp.musically", "Bundle ID of the app to download from IPAGo server")
	host := flag.String("h", "", "Host IPAGo server")
	port := flag.Int("p", 0, "Port IPAGo server")
	udid := flag.String("u", "", "UDID of the device to download the app to")
	user := flag.String("user", "", "User to login to IPAGo server")
	pswd := flag.String("pswd", "", "Password to login to IPAGo server")
	query := flag.String("q", "", "Query to search for apps on IPAGo server (example: TikTok)")
	limit := flag.Int("l", 1, "Limit of apps to search for on IPAGo server")
	outputPath := flag.String("o", "", "Output path to save the app")
	flag.Parse()

	if *outputPath == "" {
		fmt.Println("output path is required")
		flag.PrintDefaults()
		os.Exit(1)
	}

	os.MkdirAll(*outputPath, 0755)

	if *host == "" {
		fmt.Println("host is required")
		flag.PrintDefaults()
		os.Exit(1)
	}
	if *port == 0 {
		fmt.Println("port is required")
		flag.PrintDefaults()
		os.Exit(1)
	}
	if *udid == "" {
		fmt.Println("UDID is required")
		flag.PrintDefaults()
		os.Exit(1)
	}
	if *user == "" {
		fmt.Println("user is required")
		flag.PrintDefaults()
		os.Exit(1)
	}
	if *pswd == "" {
		fmt.Println("password is required")
		flag.PrintDefaults()
		os.Exit(1)
	}
	if *query == "" {
		fmt.Println("query is required")
		flag.PrintDefaults()
		os.Exit(1)
	}
	userQuery := url.QueryEscape(*user)
	pswdQuery := url.QueryEscape(*pswd)

	for {
		if !authInfo(*host, *port, userQuery, pswdQuery) {
			continue
		}

		if !authLogin(*host, *port, userQuery, pswdQuery) {
			continue
		}

		searchResult, err := search(*host, *port, *query, *limit)
		if err != nil {
			continue
		}

		// TODO: handle multiple apps with the same bundleID
		srFirst := searchResult[0]
		if srFirst.BundleID != *bundleID {
			fmt.Println("bundleID mismatch, expected", *bundleID, "got", srFirst.BundleID)
			time.Sleep(120 * time.Minute)
			continue
		}

		if checkVersionInDownloaded(*udid, *query, srFirst.Version, *outputPath) {
			fmt.Println("version already downloaded, skipping...")
			time.Sleep(120 * time.Minute)
			continue
		}

		if !download(*host, *port, srFirst.BundleID, *query, srFirst.Version, *udid, *outputPath) {
			continue
		}

	}
}
