package main

import (
	"archive/zip"
	"bufio"
	"container/list"
	"fmt"
	"github.com/beevik/etree"
	"io"
	"kavita-helper-go/jnc"
	"os"
	"os/exec"
	"regexp"
	"slices"
	"strconv"
	"strings"
)

type Chapter struct {
	numberMain int8
	numberSub  int8 // uses 0 for regular main chapters
	chDisplay  string
	title      string
	firstPage  string
	lastPage   string
}

type FileWrapper struct {
	fileName string
	file     zip.File
	fileSet  bool
}

func NewFileWrapper() FileWrapper {
	newFileWrapper := FileWrapper{}
	newFileWrapper.fileSet = false
	newFileWrapper.fileName = ""
	return newFileWrapper
}

// TODO's
// [ ] Create Chapter .cbz's based on given Information (no ComicInfo.xml yet)
// 		[ ] Sort Into Volume Folders (requires following step)
// [ ] Get ComicInfo.xml data from the J-Novel Club API
// [ ] Get the Manga from the J-Novel Club API

func GetUserInput(prompt string) string {
	reader := bufio.NewScanner(os.Stdin)
	fmt.Print(prompt)
	reader.Scan()
	err := reader.Err()
	if err != nil {
		panic(err)
	}
	return reader.Text()
}

func ClearScreen() {
	cmd := exec.Command("clear")
	cmd.Stdout = os.Stdout
	if err := cmd.Run(); err != nil {
		fmt.Println("[ERROR] - ", err)
	}
}

func main() {
	downloadDir := "/home/neshura/Documents/Go Workspaces/Kavita Helper/"

	// initialize the J-Novel Club Instance
	jnovel := jnc.NewJNC()

	var username string
	if slices.Contains(os.Args, "-u") {
		idx := slices.Index(os.Args, "-u")
		username = os.Args[idx+1]
	} else {
		username = GetUserInput("Enter Username: ")
	}
	jnovel.SetUsername(username)

	var password string
	if slices.Contains(os.Args, "-p") {
		idx := slices.Index(os.Args, "-p")
		password = os.Args[idx+1]
	} else {
		password = GetUserInput("Enter Password: ")
	}
	jnovel.SetPassword(password)

	err := jnovel.Login()
	if err != nil {
		panic(err)
	}
	defer func(jnovel *jnc.Api) {
		err := jnovel.Logout()
		if err != nil {
			panic(err)
		}
	}(&jnovel)

	err = jnovel.FetchLibrary()
	if err != nil {
		panic(err)
	}

	err = jnovel.FetchLibrarySeries()
	if err != nil {
		panic(err)
	}

	seriesList, err := jnovel.GetLibrarySeries()
	if err != nil {
		panic(err)
	}

	ClearScreen()

	fmt.Println("[1] - Download Single Volume")
	fmt.Println("[2] - Download Entire Series")
	fmt.Println("[3] - Download Entire Library")
	fmt.Println("[4] - Download Entire Library [MANGA only]")
	fmt.Println("[5] - Download Entire Library [NOVEL only]")
	mode := GetUserInput("Select Mode: ")

	ClearScreen()

	if mode == "3" || mode == "4" || mode == "5" {
		updatedOnly := GetUserInput("Download Only Where Newer Files Are Available? [Y/N]: ")

		for s := range seriesList {
			serie := seriesList[s]
			if mode == "3" || mode == "4" {
				if serie.Info.Type == "MANGA" {
					HandleSeries(jnovel, serie, downloadDir, updatedOnly == "Y" || updatedOnly == "y")
				}
			}
			if mode == "3" || mode == "5" {
				if serie.Info.Type == "NOVEL" {
					HandleSeries(jnovel, serie, downloadDir, updatedOnly == "Y" || updatedOnly == "y")
				}
			}
		}
		fmt.Println("Done")
	} else {
		fmt.Println("\n###[Series Selection]###")
		for s := range seriesList {
			serie := seriesList[s].Info

			fmt.Printf("[%s] - [%s] - %s\n", strconv.Itoa(s+1), serie.Type, serie.Title)
		}

		msg := "Enter Series Number: "
		var seriesNumber int
		for {
			selection := GetUserInput(msg)
			seriesNumber, err = strconv.Atoi(selection)
			if err != nil {
				msg = "\rInvalid. Enter VALID Series Number: "
			}
			break
		}

		serie := seriesList[seriesNumber-1]

		if mode == "2" {
			HandleSeries(jnovel, serie, downloadDir, false)
		} else {
			ClearScreen()
			fmt.Println("\n###[Volume Selection]###")
			for i := range serie.Volumes {
				vol := serie.Volumes[i]
				fmt.Printf("[%s] - %s\n", strconv.Itoa(i+1), vol.Info.Title)
			}

			msg = "Enter Volume Number: "
			var volumeNumber int
			for {
				selection := GetUserInput(msg)
				volumeNumber, err = strconv.Atoi(selection)
				if err != nil {
					msg = "\rInvalid. Enter VALID Volume Number: "
				}
				break
			}

			volume := serie.Volumes[volumeNumber-1]
			HandleVolume(jnovel, serie, volume, downloadDir)
		}
	}
}

func HandleSeries(jnovel jnc.Api, serie jnc.SerieAugmented, downloadDir string, updatedOnly bool) {
	for v := range serie.Volumes {
		volume := serie.Volumes[v]
		if len(volume.Downloads) != 0 && volume.UpdateAvailable() {
			downloadDir = PrepareSerieDirectory(serie, volume, downloadDir)
			HandleVolume(jnovel, serie, volume, downloadDir)
		}
	}
}

func PrepareSerieDirectory(serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadDir string) string {
	splits := strings.Split(downloadDir, "/")
	// last element of this split is always empty due to the trailing slash
	if splits[len(splits)-2] != serie.Info.Title {
		if serie.Info.Title == "Ascendance of a Bookworm" {
			partSplits := strings.Split(volume.Info.ShortTitle, " ")
			part := " " + partSplits[0] + " " + partSplits[1]

			if strings.Split(splits[len(splits)-2], " ")[0] == "Ascendance" {
				splits[len(splits)-2] = serie.Info.Title + part
			} else {
				splits[len(splits)-1] = serie.Info.Title + part
				splits = append(splits, "")
			}
			downloadDir = strings.Join(splits, "/")
		} else {
			downloadDir += serie.Info.Title + "/"
		}

		_, err := os.Stat(downloadDir)
		if err != nil {
			err = os.Mkdir(downloadDir, 0755)
			if err != nil {
				panic(err)
			}
		}
	}

	return downloadDir
}

func HandleVolume(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadDir string) {
	var downloadLink string
	if len(volume.Downloads) == 0 {
		fmt.Printf("Volume %s currently has no downloads available. Skipping \n", volume.Info.Title)
		return
	} else if len(volume.Downloads) > 1 {
		fmt.Println("TODO: implement download selection/default select highest quality")
		downloadLink = volume.Downloads[len(volume.Downloads)-1].Link
	} else {
		downloadLink = volume.Downloads[0].Link
	}

	DownloadAndProcessEpub(jnovel, serie, volume, downloadLink, downloadDir)
}

func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadLink string, downloadDirectory string) {
	FormatNavigationFile := map[string]string{
		"manga": "item/nav.ncx",
		"novel": "OEBPS/toc.ncx",
	}

	FormatMetadataName := map[string]string{
		"item/comic.opf":    "manga",
		"OEBPS/content.opf": "novel",
	}

	MetaInf := "META-INF/container.xml"
	file, err := jnovel.Download(downloadLink, downloadDirectory)
	if err != nil {
		panic(err)
	}

	epub, err := zip.OpenReader(file)
	if err != nil {
		fmt.Println(err)
	}

	metadata := NewFileWrapper()
	navigation := NewFileWrapper()

	for i := range epub.Reader.File {
		file := epub.Reader.File[i]

		if file.Name == MetaInf {
			doc := etree.NewDocument()
			reader, err := file.Open()
			if err != nil {
				fmt.Println(err)
			}

			if _, err := doc.ReadFrom(reader); err != nil {
				_ = reader.Close()
				panic(err)
			}

			metadata.fileName = doc.FindElement("//container/rootfiles/rootfile").SelectAttr("full-path").Value
			navigation.fileName = FormatNavigationFile[FormatMetadataName[metadata.fileName]]
		}

		if len(metadata.fileName) != 0 && file.Name == metadata.fileName {
			metadata.file = *file
			metadata.fileSet = true
		}

		if len(navigation.fileName) != 0 && file.Name == navigation.fileName {
			navigation.file = *file
			navigation.fileSet = true
		}

		if metadata.fileSet && navigation.fileSet {
			println("breaking")
			break
		}
	}

	// Switch based on metadata file

	switch FormatMetadataName[metadata.fileName] {
	case "manga":
		{
			chapterList := GenerateMangaChapterList(navigation)

			for ch := chapterList.Front(); ch != nil; ch = ch.Next() {
				chap := ch.Value.(Chapter)
				fmt.Printf("[%s] %s: %s - %s\n", chap.chDisplay, chap.title, chap.firstPage, chap.lastPage)
			}
		}
	case "novel":
		{
			CalibreSeriesAttr := "calibre:series"
			CalibreSeriesIndexAttr := "calibre:series_index"
			EpubTitleTag := "//package/metadata"

			metafile, err := metadata.file.Open()
			if err != nil {
				fmt.Println(err)
			}

			doc := etree.NewDocument()
			if _, err = doc.ReadFrom(metafile); err != nil {
				fmt.Println(err)
			}

			idx := doc.FindElement(EpubTitleTag + "/dc:title").Index()

			seriesTitle := etree.NewElement("meta")
			seriesTitle.CreateAttr("name", CalibreSeriesAttr)
			var title string
			var number string
			if serie.Info.Title == "Ascendance of a Bookworm" {
				splits := strings.Split(volume.Info.ShortTitle, " ")
				title = serie.Info.Title + " " + splits[0] + " " + splits[1]
				number = splits[3]
			} else {
				title = serie.Info.Title
				number = strconv.Itoa(volume.Info.Number)
			}

			seriesTitle.CreateAttr("content", title)

			seriesNumber := etree.NewElement("meta")
			seriesNumber.CreateAttr("name", CalibreSeriesIndexAttr)
			seriesNumber.CreateAttr("content", number)

			doc.FindElement(EpubTitleTag).InsertChildAt(idx+1, seriesTitle)
			doc.FindElement(EpubTitleTag).InsertChildAt(idx+2, seriesNumber)
			doc.Indent(2)

			// Open the zip file for writing
			zipfile, err := os.OpenFile(file+".new", os.O_RDWR|os.O_CREATE, 0644)
			if err != nil {
				panic(err)
			}
			defer func(zipfile *os.File) {
				err := zipfile.Close()
				if err != nil {
					panic(err)
				}
			}(zipfile)

			zipWriter := zip.NewWriter(zipfile)

			str, err := doc.WriteToString()
			if err != nil {
				panic(err)
			}

			metawriter, err := zipWriter.Create(metadata.fileName)
			if err != nil {
				panic(err)
			}

			_, err = metawriter.Write([]byte(str))
			if err != nil {
				panic(err)
			}

			for _, f := range epub.File {
				if f.Name != metadata.fileName {
					writer, err := zipWriter.Create(f.Name)
					if err != nil {
						panic(err)
					}
					reader, err := f.Open()
					if err != nil {
						panic(err)
					}

					_, err = io.Copy(writer, reader)
					if err != nil {
						_ = reader.Close()
						panic(err)
					}
				}
			}

			epub.Close()
			zipWriter.Close()
			source, _ := os.Open(file + ".new")
			dest, _ := os.Create(file)
			io.Copy(dest, source)
			os.Remove(file + ".new")
		}
	}
}

func FinalizeChapters(chapters *list.List, pageList []*etree.Element) *list.List {
	fmt.Println("Finalizing Chapters")

	lastPage := pageList[len(pageList)-1].SelectAttr("src").Value

	sortedChapters := list.New()

	for ch := chapters.Back(); ch != nil; ch = ch.Prev() {
		var newChapterData Chapter
		currentChapter := ch.Value.(Chapter)

		if currentChapter.numberMain != -1 {
			newChapterData.title = currentChapter.title
			newChapterData.firstPage = currentChapter.firstPage
		} else {
			mainChapter := GetLastValidChapterNumber(ch)
			for sch := sortedChapters.Front(); sch != nil; sch = sch.Next() {
				if sch.Value.(Chapter).title == mainChapter.title {
					newChapterData = sch.Value.(Chapter)
					newChapterData.firstPage = currentChapter.firstPage
					sch.Value = newChapterData
					break
				}
			}
			continue
		}

		if currentChapter.numberSub != 0 {
			numberMain, numberSub := AnalyzeMainChapter(ch)

			newChapterData.numberMain = numberMain
			newChapterData.numberSub = numberSub
		} else {
			newChapterData.numberMain = currentChapter.numberMain
			newChapterData.numberSub = currentChapter.numberSub
		}

		if ch.Next() == nil {
			newChapterData.lastPage = lastPage
		} else {
			var lastChapterPage string
			for p := range pageList {
				page := pageList[p]
				if page.SelectAttr("src").Value == ch.Next().Value.(Chapter).firstPage {
					lastChapterPage = pageList[p-1].SelectAttr("src").Value
				}
			}
			newChapterData.lastPage = lastChapterPage
		}

		if newChapterData.numberSub != 0 {
			newChapterData.chDisplay = strconv.FormatInt(int64(newChapterData.numberMain), 10) + "." + strconv.FormatInt(int64(newChapterData.numberSub), 10)
		} else {
			newChapterData.chDisplay = strconv.FormatInt(int64(newChapterData.numberMain), 10)
		}

		sortedChapters.PushFront(newChapterData)
	}

	return sortedChapters
}

func AnalyzeMainChapter(currentChapter *list.Element) (int8, int8) {
	var currentChapterNumber int8
	subChapterCount := 1

	if currentChapter.Next() != nil {
		ch := currentChapter.Next()
		for {
			if ch.Value.(Chapter).numberSub != 0 {
				subChapterCount++
				if ch.Next() != nil {
					ch = ch.Next()
				} else {
					break
				}
			} else {
				break
			}
		}
	}

	if currentChapter.Prev() != nil {
		ch := currentChapter.Prev()
		// Then Get the Current Main Chapter
		for {
			if ch.Value.(Chapter).numberSub != 0 {
				subChapterCount++
				if ch.Prev() != nil {
					ch = ch.Prev()
				} else {
					break
				}
			} else {
				subChapterCount++ // actual Chapter also counts in this case
				currentChapterNumber = ch.Value.(Chapter).numberMain
				break
			}
		}
	}

	// Calculate integer sub number based on total subChapterC
	subChapterNumber := int8((float32(currentChapter.Value.(Chapter).numberSub) / float32(subChapterCount)) * 10)

	// return main Chapter Number + CurrentChapter Sub Number
	return currentChapterNumber, subChapterNumber
}

func GetLastValidChapterNumber(currentChapter *list.Element) Chapter {
	for {
		if currentChapter.Next() != nil {
			currentChapter = currentChapter.Next()
			chapterData := currentChapter.Value.(Chapter)
			if chapterData.numberMain != -1 && chapterData.numberSub == 0 {
				return chapterData
			}
		}
		break
	}
	return currentChapter.Value.(Chapter)
}

func GenerateMangaChapterList(navigation FileWrapper) *list.List {
	reader, err := navigation.file.Open()
	if err != nil {
		fmt.Println(err)
	}

	doc := etree.NewDocument()
	if _, err = doc.ReadFrom(reader); err != nil {
		fmt.Println(err)
	}

	navMap := doc.FindElement("//ncx/navMap")
	navChapters := navMap.FindElements("//navPoint")
	chapters := list.New()

	pageList := doc.FindElements("//ncx/pageList/pageTarget/content")

	lastMainChapter := int8(-1)
	subChapter := int8(0)

	for c := range len(navChapters) {
		navChapter := navChapters[c]
		label := navChapter.FindElement("./navLabel/text").Text()
		page := navChapter.FindElement("./content")

		regex := "Chapter ([0-9]*)"
		match, _ := regexp.MatchString(regex, label)

		if match {
			r, _ := regexp.Compile(regex)
			num := r.FindStringSubmatch(label)[1]
			parse, _ := strconv.ParseInt(num, 10, 8)
			lastMainChapter = int8(parse)
			subChapter = int8(0)
		} else {
			if lastMainChapter == -1 {
				subChapter -= 1
			} else {
				subChapter += 1
			}
		}

		chapterData := Chapter{
			numberMain: lastMainChapter,
			numberSub:  subChapter,
			chDisplay:  "",
			title:      label,
			firstPage:  page.SelectAttr("src").Value,
			lastPage:   "",
		}

		chapters.PushBack(chapterData)
	}

	return FinalizeChapters(chapters, pageList)
}