package jnc

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"slices"
	"sort"
	"strconv"
	"strings"
	"time"
)

const BaseUrl = "https://labs.j-novel.club/"
const ApiV1Url = BaseUrl + "app/v1/"
const ApiV2Url = BaseUrl + "app/v2/"
const FeedUrl = BaseUrl + "feed/"

type ApiFormat int

const (
	JSON = iota
	TEXT
	PROTOBUF
)

var ApiFormatParam = map[ApiFormat]string{
	JSON:     "format=json",
	TEXT:     "format=text",
	PROTOBUF: "",
}

type BookStatus string
type PublishingStatus string
type SeriesFormat string
type DownloadType string

type AuthBody struct {
	Login    string `json:"login"`
	Password string `json:"password"`
	Slim     bool   `json:"slim"`
}

type JWT struct {
	Token   string `json:"id"`
	TTL     string `json:"ttl"`
	Created string `json:"created"`
}

type Api struct {
	_auth    Auth
	_format  ApiFormat
	_library Library
	_series  map[string]SerieAugmented
}

type Auth struct {
	_username string
	_password string
	_jwt      JWT
}

type Library struct {
	Books      []Book     `json:"books"`
	Pagination Pagination `json:"pagination"`
}

type Book struct {
	Id           string     `json:"id"`
	LegacyId     string     `json:"legacyId"`
	Volume       Volume     `json:"volume"`
	Serie        Serie      `json:"serie"`
	Purchased    string     `json:"purchased"`
	LastDownload string     `json:"lastDownload"`
	LastUpdated  string     `json:"lastUpdated"`
	Downloads    []Download `json:"downloads"`
	Status       BookStatus `json:"status"`
}

type Volume struct {
	Id                string   `json:"id"`
	LegacyId          string   `json:"legacyId"`
	Title             string   `json:"title"`
	ShortTitle        string   `json:"shortTitle"`
	OriginalTitle     string   `json:"originalTitle"`
	Slug              string   `json:"slug"`
	Number            int      `json:"number"`
	OriginalPublisher string   `json:"originalPublisher"`
	Label             string   `json:"label"`
	Creators          Creators `json:"creators"` // TODO: check, might not be correct
	hidden            bool
	ForumTopicId      int    `json:"forumTopicId"`
	Created           string `json:"created"`
	Publishing        string `json:"publishing"`
	Description       string `json:"description"`
	ShortDescription  string `json:"shortDescription"`
	Cover             Cover  `json:"cover"`
	Owned             bool   `json:"owned"`
	PremiumExtras     string `json:"premiumExtras"`
	NoEbook           bool   `json:"noEbook"`
	TotalParts        int    `json:"totalParts"`
	OnSale            bool   `json:"onSale"`
}

type Creators []Creator

type Creator struct {
	Id           string `json:"id"`
	Name         string `json:"name"`
	Role         string `json:"role"`
	OriginalName string `json:"originalName	"`
}

func (creators *Creators) Contains(name string) bool {
	return slices.ContainsFunc(*creators, func(c Creator) bool {
		return c.Name == name
	})
}

func (creators *Creators) Get(name string) *Creator {
	idx := slices.IndexFunc(*creators, func(c Creator) bool {
		return c.Name == name
	})
	return &(*creators)[idx]
}

type VolumeAugmented struct {
	Info       Volume
	Downloads  []Download
	updated    string
	downloaded string
}

func (volume *VolumeAugmented) UpdateAvailable() bool {
	if volume.downloaded == "" && len(volume.Downloads) != 0 {
		return true // The file has never been downloaded but at least one is available
	}

	downloadedTime, err := time.Parse(time.RFC3339, volume.downloaded)
	if err != nil {
		panic(err)
	}

	updatedTime, err := time.Parse(time.RFC3339, volume.updated)
	if err != nil {
		panic(err)
	}

	if downloadedTime.Before(updatedTime) {
		return true
	} else {
		return false
	}
}

type Cover struct {
	OriginalUrl  string `json:"originalUrl"`
	CoverUrl     string `json:"coverUrl"`
	ThumbnailUrl string `json:"thumbnail"`
}

type Serie struct {
	Id               string           `json:"id"`
	LegacyId         string           `json:"legacyId"`
	Type             SeriesFormat     `json:"type"`
	Status           PublishingStatus `json:"status"`
	Title            string           `json:"title"`
	ShortTitle       string           `json:"shortTitle"`
	OriginalTitle    string           `json:"originalTitle"`
	Slug             string           `json:"slug"`
	Hidden           bool             `json:"hidden"`
	Created          string           `json:"created"`
	Description      string           `json:"description"`
	ShortDescription string           `json:"shortDescription"`
	Tags             []string         `json:"tags"`
	Cover            Cover            `json:"cover"`
	Following        bool             `json:"following"`
	Catchup          bool             `json:"catchup"`
	Rentals          bool             `json:"rentals"`
	TopicId          int              `json:"topicId"`
	AgeRating        int              `json:"ageRating"`
}

type SerieAugmented struct {
	Info    Serie             `json:"series"`
	Volumes []VolumeAugmented `json:"volumes"`
}

type Download struct {
	Link  string       `json:"link"`
	Type  DownloadType `json:"type"`
	Label string       `json:"label"`
}

type Pagination struct {
	Limit    int  `json:"limit"`
	Skip     int  `json:"skip"`
	LastPage bool `json:"lastPage"`
}

func NewJNC() Api {
	jncApi := Api{
		_auth: Auth{
			_username: "",
			_password: "",
			_jwt:      JWT{},
		},
		_library: Library{},
		_series:  map[string]SerieAugmented{},
	}
	return jncApi
}

func (jncApi *Api) ReturnFormat() string {
	return ApiFormatParam[jncApi._format]
}

// Login attempts a login using the username and password set with SetUsername and SetPassword
func (jncApi *Api) Login() error {
	fmt.Println("Logging in...")
	if jncApi._auth._username == "" {
		return errors.New("username not specified")
	}
	if jncApi._auth._password == "" {
		return errors.New("password not specified")
	}

	authBody := AuthBody{
		Login:    jncApi._auth._username,
		Password: jncApi._auth._password,
		Slim:     true,
	}

	jsonAuthBody, err := json.Marshal(authBody)
	if err != nil {
		return err
	}

	res, err := http.Post(ApiV2Url+"auth/login?"+jncApi.ReturnFormat(), "application/json", bytes.NewBuffer(jsonAuthBody))
	if err != nil {
		return err
	}
	if res.StatusCode != 200 && res.StatusCode != 201 {
		return errors.New(res.Status)
	}

	body, err := io.ReadAll(res.Body)
	if err != nil {
		return err
	}
	err = json.Unmarshal(body, &jncApi._auth._jwt)
	if err != nil {
		return err
	}
	return nil
}

// LoginOTP starts an OTP based login using the username provided with SetUsername. NOTE: Currently not implemented
func (jncApi *Api) LoginOTP() error {
	if jncApi._auth._username == "" {
		return errors.New("username not specified")
	}

	return errors.New("not implemented")
}

func (jncApi *Api) AuthParam() string {
	return "access_token=" + jncApi._auth._jwt.Token
}

// Logout invalidates the auth token and resets the jnc instance to a blank slate. No information remains after calling.
func (jncApi *Api) Logout() error {
	fmt.Println("Logging out...")
	res, err := http.Post(ApiV2Url+"auth/logout?"+jncApi.ReturnFormat()+"&"+jncApi.AuthParam(), "application/json", nil)
	if err != nil {
		panic(err)
	}

	if res.StatusCode != 204 {
		return errors.New(res.Status)
	}

	*jncApi = Api{
		_auth:    Auth{},
		_format:  JSON,
		_library: Library{},
		_series:  map[string]SerieAugmented{},
	}

	return nil
}

func (jncApi *Api) SetPassword(password string) {
	jncApi._auth._password = password
}

func (jncApi *Api) SetUsername(username string) {
	jncApi._auth._username = username
}

// FetchLibrary retrieves the list of Book's in the logged-in User's J-Novel Club library
func (jncApi *Api) FetchLibrary() error {
	fmt.Println("Fetching library contents...")
	res, err := http.Get(ApiV2Url + "me/library?" + jncApi.ReturnFormat() + "&" + jncApi.AuthParam())
	if err != nil {
		return err
	}

	if res.StatusCode != 200 {
		return errors.New(res.Status)
	}

	body, err := io.ReadAll(res.Body)
	if err != nil {
		return err
	}
	err = json.Unmarshal(body, &jncApi._library)
	if err != nil {
		return err
	}

	for i := range jncApi._library.Books {
		book := &jncApi._library.Books[i]

		data, ok := jncApi._series[book.Serie.Id]
		if ok {
			data.Volumes = append(data.Volumes, VolumeAugmented{
				Info:       book.Volume,
				Downloads:  book.Downloads,
				updated:    book.LastUpdated,
				downloaded: book.LastDownload,
			})
			sort.Slice(data.Volumes, func(i, j int) bool {
				return data.Volumes[i].Info.Number < data.Volumes[j].Info.Number
			})
			jncApi._series[book.Serie.Id] = data
			continue
		}
		jncApi._series[book.Serie.Id] = SerieAugmented{
			Info: book.Serie,
			Volumes: []VolumeAugmented{
				{
					Info:       book.Volume,
					Downloads:  book.Downloads,
					updated:    book.LastUpdated,
					downloaded: book.LastDownload,
				},
			},
		}
	}

	return nil
}

// FetchLibrarySeries is only needed because for whatever reason the Endpoint used in FetchLibrary does not return the Series Tags
func (jncApi *Api) FetchLibrarySeries() error {
	progress := 1
	fmt.Printf("Fetching Series Info: 0/%s - 0%", strconv.Itoa(len(jncApi._series)))
	for i := range jncApi._series {
		fmt.Printf("\rFetching Series Info: %s/%s - %s%%", strconv.Itoa(progress), strconv.Itoa(len(jncApi._series)), strconv.FormatFloat(float64(progress)/float64(len(jncApi._series))*float64(100), 'f', 2, 32))
		serie := jncApi._series[i]

		res, err := http.Get(ApiV2Url + "series/" + serie.Info.Id + "?" + jncApi.ReturnFormat() + "&" + jncApi.AuthParam())

		if err != nil {
			return err
		}

		if res.StatusCode != 200 {
			return errors.New(res.Status)
		}

		body, err := io.ReadAll(res.Body)
		if err != nil {
			return err
		}

		err = json.Unmarshal(body, &serie)
		if err != nil {
			jncApi._series[i] = serie
		}

		progress++
	}

	return nil
}

// FetchVolumeInfo retrieves additional Volume Info that was not returned when retrieving the entire Library
func (jncApi *Api) FetchVolumeInfo(volume Volume) (Volume, error) {
	fmt.Println("Fetching Volume details...")
	res, err := http.Get(ApiV2Url + "volumes/" + volume.Id + "?" + jncApi.ReturnFormat() + "&" + jncApi.AuthParam())
	if err != nil {
		return volume, err
	}

	if res.StatusCode != 200 {
		return volume, errors.New(res.Status)
	}

	body, err := io.ReadAll(res.Body)
	if err != nil {
		return volume, err
	}
	err = json.Unmarshal(body, &volume)
	if err != nil {
		return volume, err
	}

	return volume, nil
}

// GetLibrarySeries returns a list of unique series found in the User's library. Tags are only included if FetchLibrarySeries was previously called.
func (jncApi *Api) GetLibrarySeries() (seriesList []SerieAugmented, err error) {
	arr := make([]SerieAugmented, 0, len(jncApi._series))
	for s := range jncApi._series {
		arr = append(arr, jncApi._series[s])
	}

	sort.Slice(arr, func(i, j int) bool {
		return arr[i].Info.Title < arr[j].Info.Title
	})

	return arr, nil
}

func (jncApi *Api) Download(link string, targetDir string) (name string, err error) {
	fmt.Printf("Downloading %s...\n", link)
	res, err := http.Get(link)
	if err != nil {
		return "", err
	}
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(res.Body)

	if res.StatusCode != 200 {
		return "", errors.New(res.Status)
	}

	filePath := targetDir + strings.Trim(strings.Split(res.Header["Content-Disposition"][0], "=")[1], "\"")

	file, err := os.Create(filePath)
	if err != nil {
		return "", err
	}
	defer func(file *os.File) {
		err := file.Close()
		if err != nil {
			panic(err)
		}
	}(file)

	_, err = io.Copy(file, res.Body)
	if err != nil {
		return "", err
	}

	return filePath, nil
}