package cmd

import (
	"io"
	"io/fs"
	"lon-tool/utils"
	"lon-tool/image"
	"net"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/pterm/pterm"
	"github.com/spf13/cobra"
	"github.com/timoxa0/goadb/adb"
	"github.com/timoxa0/gofastboot/fastboot"
)

var username string
var password string
var serail string
var partsize string
var partpercent int
var deployCmd = &cobra.Command{
	Use:   "deploy <rootfs.lni>",
	Short: "Deploy system to device",
	Long:  "Deploy system to device in fastboot mode",
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		var msg string
		req_repartition := partsize != ""

		image, close, err := image.ReadImage(args[0])
		defer close()
		if err != nil {
			logger.Error("Failed to read image info:", logger.Args("Error", err))
			os.Exit(1)
		}

		adbc, err := adb.New()
		if err != nil {
			logger.Fatal("Failed to get adb client", logger.Args(err))
		}
		fb_devs, err := fastboot.FindDevices()
		if err != nil {
			logger.Fatal("Failed to get fastboot device", logger.Args(err))
		}

		if len(fb_devs) < 1 {
			logger.Fatal("No fastboot devices found")
			os.Exit(170)
		}

		if serail == "autodetect" {
			for _, dev := range fb_devs {
				product, err := dev.GetVar("product")
				if err != nil {
					logger.Warn("Unable to communicate with device", logger.Args("Serial", dev.Serial))
					continue
				}
				logger.Debug("Found device", logger.Args("Product", product, "Sraial", dev.Serial))
				if product == "nabu" {
					logger.Debug("Nabu found", logger.Args("Serial", dev.Serial))
					serail = dev.Serial
					if !req_repartition {
						_, err1 := dev.GetVar("partition-type:linux")
						_, err2 := dev.GetVar("partition-type:esp")
						req_repartition = err1 != nil && err2 != nil
					}
					break
				}
			}
			if serail == "autodetect" {
				logger.Fatal("Nabu in fastboot mode not found")
				os.Exit(170)
			}
		}

		for {
			if username != "" {
				break
			}
			username, _ = pterm.DefaultInteractiveTextInput.Show("Linux username")
			if r, _ := regexp.MatchString(`[A-z0-9]+`, username); !r {
				username = ""
				logger.Error("Username must contain only latin letter or digits")
			}
		}

		for {
			if password != "" {
				break
			}
			password, _ = pterm.DefaultInteractiveTextInput.WithMask("*").Show("Linux password")
			if r, _ := regexp.MatchString(`[A-z0-9$%#@!]+`, password); !r {
				password = ""
				logger.Error("Password must contain only latin letter, digits or $%#@!")
			}
		}

		var run_repartition bool
		if req_repartition {
			run_repartition, _ = pterm.DefaultInteractiveConfirm.WithDefaultValue(true).Show("Found incompatible partition table. Do you want to change it?")
		} else {
			run_repartition, _ = pterm.DefaultInteractiveConfirm.Show("Found compatible partition table. Do you want to change it?")
		}

		for {
			if partsize != "" || !run_repartition {
				break
			}
			partsize, _ = pterm.DefaultInteractiveTextInput.WithDefaultValue("50%").Show("Partition size [20;90]%")
			v, err := strconv.Atoi(strings.TrimRight(partsize, "%"))
			partpercent = v
			if !(err == nil && 20 <= v && v <= 90 && partsize[len(partsize)-1] == '%') {
				partsize = ""
				logger.Error("Invalid partsize. Parsize must be in percents")
			}
		}

		msg, _ = pterm.DefaultTree.WithRoot(pterm.TreeNode{
			Text: pterm.Green("System"),
			Children: []pterm.TreeNode{
				{Text: pterm.Sprintf("Name: %s", image.Name)},
				{Text: pterm.Sprintf("Version: %s", image.Version)},
			},
		}).Srender()
		pterm.Print(msg)
		msg, _ = pterm.DefaultTree.WithRoot(pterm.TreeNode{
			Text: pterm.Green("Settings"),
			Children: []pterm.TreeNode{
				{Text: pterm.Sprintf("Username: %s", username)},
				{Text: pterm.Sprintf("Password: %s", password)},
				{Text: pterm.Sprintf("Device serial: %s", serail)},
				{Text: pterm.Sprintf("Run repatition: %v", map[bool]string{
					true:  pterm.Green("Yes"),
					false: pterm.Red("No"),
				}[run_repartition])},
				{Text: pterm.Sprintf("Partition size: %s", map[bool]string{
					true:  "Not changed",
					false: partsize,
				}[partsize == ""])},
			},
		}).Srender()
		pterm.Print(msg)
		var isok bool
		if run_repartition {
			isok, _ = pterm.DefaultInteractiveConfirm.Show("Start installation? All user data will be ERASED")
		} else {
			isok, _ = pterm.DefaultInteractiveConfirm.Show("Start installation?")
		}
		if !isok {
			pterm.Println("Bye")
			os.Exit(253)
		}
		fb_dev, _ := fastboot.FindDevice(serail)
		adbd := adbc.Device(adb.DeviceWithSerial(serail))
		bootdata, err := utils.Files.OrangeFox.Get(*pbar.WithTitle("Downloading orangefox"))
		if err != nil {
			if bootdata != nil {
				logger.Warn("Unable to verify recovery image")
			} else {
				logger.Error("Unable to download recovery image")
				os.Exit(179)
			}
		}
		logger.Debug("Bootdata")

		if run_repartition {
			gpt, err := utils.Files.GPT.Get(*pbar.WithTitle("Downloading default partition table"))
			if err != nil {
				if gpt != nil {
					logger.Warn("Unable to verify gpt image")
				} else {
					logger.Error("Unable to download gpt image")
					os.Exit(179)
				}
			}

			userdata, err := utils.Files.CleanUserdata.Get(*pbar.WithTitle("Downloading empty userdata"))
			if err != nil {
				if userdata != nil {
					logger.Warn("Unable to verify userdata image")
				} else {
					logger.Error("Unable to download userdata image")
					os.Exit(179)
				}
			}

			if e := fb_dev.Flash("partition:0", gpt); e != nil {
				logger.Error("Failed to flash GPT. Reflash stock rom and try again")
				logger.Debug("Error", logger.Args("e", e))
				os.Exit(180)
			}

			if e := fb_dev.Flash("userdata", userdata); e != nil {
				logger.Error("Failed to clean userdata. Reflash stock rom and try again")
				logger.Debug("Error", logger.Args("e", e))
				os.Exit(180)
			}

			ofoxSpinner, _ := spinner.Start("Booting recovery")
			if e := fb_dev.BootImage(bootdata); e != nil {
				logger.Error("Failed to boot recovery. Reflash stock rom and try again")
				logger.Debug("Error", logger.Args("e", e))
				os.Exit(177)
			}

			for i := 0; i <= 120; i++ {
				if i == 120 {
					ofoxSpinner.Stop()
					logger.Error("Recovery device timeout")
					os.Exit(173)
				}
				if s, _ := adbd.State(); s == adb.StateRecovery {
					ofoxSpinner.Stop()
					break
				}
				time.Sleep(time.Second)
			}

			block_size, _ := adbd.RunCommand("blockdev --getsize64 /dev/block/sda")
			block_size = strings.TrimRight(block_size, "\n")
			is128 := false
			if r, _ := regexp.MatchString(`^125[0-9]{9}$`, block_size); r {
				is128 = true
			} else if r, _ := regexp.MatchString(`^253[0-9]{9}$`, block_size); r {
				is128 = false
			}

			for _, cmd := range utils.GenRepartCommands(partpercent, is128) {
				adbd.RunCommand(cmd)
				logger.Debug("Executed command", logger.Args("cmd", cmd))
			}

			adbd.RunCommand("reboot bootloader")
			fbSpinner, _ := spinner.Start("Rebooting to bootloader")
			for i := 0; i <= 120; i++ {
				if i == 120 {
					fbSpinner.Stop()
					logger.Error("Fastboot device timeout")
					os.Exit(172)
				}
				if d, e := fastboot.FindDevice(serail); e == nil {
					fbSpinner.Stop()
					fb_dev = d
					break
				}
				time.Sleep(time.Second)
			}

			if e := fb_dev.Flash("userdata", userdata); e != nil {
				logger.Error("Failed to clean userdata. Reflash stock rom and try again")
				logger.Debug("Error", logger.Args("e", e))
				os.Exit(180)
			}
			logger.Info("Repartiton done")
		}

		ofoxSpinner, _ := spinner.Start("Booting recovery")
		fb_dev.BootImage(bootdata)
		for i := 0; i <= 120; i++ {
			if i == 120 {
				ofoxSpinner.Stop()
				logger.Error("Recovery device timeout")
				os.Exit(173)
			}
			if s, _ := adbd.State(); s == adb.StateRecovery {
				ofoxSpinner.Stop()
				break
			}
			time.Sleep(time.Second)
		}

		port, err := utils.GetFreePort()
		if err != nil {
			logger.Error("Failled to find free tcp port")
			os.Exit(181)
		}
		logger.Debug("Flasher", logger.Args("port", port))

		adbd.Forward(pterm.Sprintf("tcp:%v", port), "tcp:4444")
		logger.Debug("ListForwards", logger.Args(adbd.ListForwards()))
		doneChan1 := make(chan bool)
		doneChan2 := make(chan bool)
		go func() {
			adbd.RunCommand("busybox nc -w 10 -l 127.0.0.1:4444 > /dev/block/platform/soc/1d84000.ufshc/by-name/linux 2> /tmp/nclog.txt")
			logger.Debug("Busybox closed")
			doneChan1 <- true
		}()
		time.Sleep(time.Second * 3)

		go func() {
			conn, err := net.Dial("tcp", pterm.Sprintf("127.0.0.1:%v", port))
			if err != nil {
				logger.Error("Failled to connect to device")
			}
			buf := make([]byte, 409600)
			bar, _ := pbar.WithTotal(int(image.ImgSize)).WithTitle("Flashing rootfs").WithRemoveWhenDone(false).Start()
			dataLegth := 0
			for {
				n, err := image.Reader.Read(buf)
				dataLegth += n
				if err != nil && err != io.EOF && dataLegth < int(image.ImgSize) {
					logger.Error("Failed to read from GZ reader", logger.Args("error", err))
					return
				}
				if n == 0 {
					break
				}
				bar.Add(n)
				n, err = conn.Write(buf[:n])
				if n == 0 {
					logger.Error("Failed to write to device. Reflash stock rom and try again", logger.Args("err", err))
					adbd.RunCommand("reboot bootloader")
					os.Exit(179)
					break
				}
			}
			conn.Close()
			<-doneChan1
			doneChan2 <- true
		}()
		<-doneChan2
		adbd.KillForwardAll()

		out, err := adbd.RunCommand(pterm.Sprintf("postinstall %s %s > /dev/null 2>&1; echo $?", username, password))
		out = strings.TrimRight(out, "\n")
		logger.Debug("Postinstall", logger.Args("out", out, "err", err))
		if out != "0" || err != nil {
			logger.Error("Postinstall failed. Reflash stock rom and try again", logger.Args("Device error", out, "Go error", err))
		} else {
			logger.Info("System cofigured")
		}

		adbd.RunCommand("mkdir /tmp/uefi-install")
		uploadBar, _ := pbar.WithTotal(2).WithTitle("Uploading uefi files").Start()

		bootshim, err := utils.Files.UEFIBootshim.Get(*pbar.WithTitle("Downloading uefi bootshim"))
		if err != nil {
			if bootshim != nil {
				logger.Warn("Unable to verify uefi bootship image")
			} else {
				logger.Error("Unable to download uefi bootshim image")
				os.Exit(179)
			}
		}

		conn, err := adbd.OpenWrite(pterm.Sprintf("/tmp/uefi-install/%s", utils.Files.UEFIBootshim.Name), fs.FileMode(0777), adb.MtimeOfClose)
		if err != nil {
			uploadBar.Stop()
			logger.Error("Failed to send uefi bootshim", logger.Args("Error", err))
		}
		_, err = conn.Write(bootshim)
		if err != nil {
			uploadBar.Stop()
			logger.Error("Failed to send uefi bootshim", logger.Args("Error", err))
		}
		conn.Close()
		uploadBar.Add(1)

		payload, err := utils.Files.UEFIPayload.Get(*pbar.WithTitle("Downloading uefi payload"))
		if err != nil {
			if payload != nil {
				logger.Warn("Unable to verify uefi payload image")
			} else {
				logger.Error("Unable to download uefi payload image")
				os.Exit(179)
			}
		}
		conn, err = adbd.OpenWrite(pterm.Sprintf("/tmp/uefi-install/%s", utils.Files.UEFIPayload.Name), fs.FileMode(0777), adb.MtimeOfClose)
		if err != nil {
			uploadBar.Stop()
			logger.Error("Failed to send uefi payload", logger.Args("Error", err))
		}
		_, err = conn.Write(payload)
		if err != nil {
			uploadBar.Stop()
			logger.Error("Failed to send uefi payload", logger.Args("Error", err))
		}
		conn.Close()
		uploadBar.Add(1)

		uefiSpinner, _ := spinner.Start("Patching UEFI")
		out, err = adbd.RunCommand("uefi-patch  > /dev/null 2>&1; echo $?")
		out = strings.TrimRight(out, "\n")
		if err != nil {
			logger.Error("Failed to install uefi. Reflash stock rom and try again", logger.Args("Error", err))
			os.Exit(176)
		}
		logger.Debug("Uefi patch", logger.Args("Out", out))

		switch out {
		case "1":
			uefiSpinner.Stop()
			logger.Error("Failed to install uefi. Reflash stock rom and try again", logger.Args("Error", err))
			adbd.RunCommand("reboot bootloader")
			os.Exit(176)
		case "2":

			adbd.RunCommand("reboot")
			uefiSpinner.Stop()
			logger.Info("Bootimage already patched")
		case "0":
			adbd.RunCommand("reboot")
			uefiSpinner.Stop()
			logger.Info("Installation done!")
		}
	},
}

func init() {
	rootCmd.AddCommand(deployCmd)
	deployCmd.Flags().StringVarP(&username, "username", "u", "", "User name")
	deployCmd.Flags().StringVarP(&password, "password", "p", "", "User password")
	deployCmd.Flags().StringVarP(&serail, "serial", "s", "autodetect", "Device serial")
	deployCmd.Flags().StringVarP(&partsize, "part-size", "S", "", "Linux partition size in percents")
}