package main

import (
	"archive/zip"
	"bytes"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"embed"
	"encoding/base64"
	"encoding/pem"
	"encoding/xml"
	"errors"
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"io"
	"io/fs"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/gen2brain/go-fitz"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
	"github.com/golang/freetype/truetype"
	"github.com/google/uuid"
	"github.com/joho/godotenv"
	"golang.org/x/crypto/bcrypt"
	"golang.org/x/image/font"
	"golang.org/x/image/math/fixed"
	"golang.org/x/net/html/charset"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"

	"mi6e4ka/yabl-api/reaper"
	"mi6e4ka/yabl-api/schemas"
)

type Search struct {
	Count int       `json:"count"`
	Time  float64   `json:"time"`
	Books []APIBook `json:"books"`
}
type Person struct {
	Id         int     `json:"id"`
	Name       string  `json:"name"`
	FirstName  string  `json:"firstName"`
	MiddleName *string `json:"middleName"`
	LastName   *string `json:"lastName"`
}
type Books struct {
	Title        string               `json:"title"`
	Authors      []Person             `json:"authors"`
	Sequence     *string              `json:"sequence"`
	IsTranslated bool                 `json:"is_translated"`
	Translators  *[]Person            `json:"translators"`
	SrcLang      *string              `json:"src_lang"`
	Lang         *string              `json:"lang"`
	Isbn         *string              `json:"isbn"`
	Downloads    int                  `json:"downloads"`
	Publisher    *string              `json:"publisher"`
	Year         *int                 `json:"year"`
	Genres       *[]Person            `json:"genres"`
	Description  *string              `json:"description"`
	Size         int                  `json:"size"`
	Hash         *string              `json:"hash"`
	Bookcase     *string              `json:"bookcase"`
	Filename     string               `json:"filename"`
	Collections  []ShortAPICollection `json:"collections"`
	Filetype     string               `json:"filetype"`
}
type ShortAPICollection struct {
	ID   uint   `json:"id"`
	Name string `json:"name"`
}

func (ShortAPICollection) TableName() string {
	return "collections"
}

type Image struct {
	Id  string `xml:"id,attr"`
	B64 string `xml:",chardata"`
}
type FB2Images struct {
	Cover struct {
		Id string `xml:"href,attr"`
	} `xml:"description>title-info>coverpage>image"`
	Images []Image `xml:"binary"`
}

func genCover(title string) []byte {
	img := image.NewRGBA(image.Rect(0, 0, 240*2, 380*2))
	bgColor := color.RGBA{0, 0, 0, 255}
	draw.Draw(img, img.Bounds(), &image.Uniform{C: bgColor},
		image.Point{}, draw.Src)
	col := color.RGBA{255, 255, 255, 255}
	point := fixed.Point26_6{X: fixed.I(40), Y: fixed.I(80)}
	fontFile, _ := embeddedFonts.ReadFile("fonts/OpenSans-Regular.ttf")
	var f *truetype.Font
	f, _ = truetype.Parse(fontFile)
	opts := &truetype.Options{
		Size: 36,
	}
	face := truetype.NewFace(f, opts)
	d := &font.Drawer{
		Dst:  img,
		Src:  image.NewUniform(col),
		Face: face,
		Dot:  point,
	}
	_ = title
	d.DrawString(title)

	buf := new(bytes.Buffer)
	png.Encode(buf, img)
	return buf.Bytes()
}
func personToString(firstName string, middleName *string, lastName *string) string {
	var fullName string = firstName
	if middleName != nil {
		fullName += " " + *middleName
	}
	if lastName != nil {
		fullName += " " + *lastName
	}
	return fullName
}
func AuthMiddleware(publicKey *rsa.PublicKey) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		if ctx.FullPath() == "/api/search" {
			ctx.Next()
		}
		tokenString, err := ctx.Cookie("token")
		if err != nil {
			ctx.AbortWithStatus(401)
			return
		}
		claims := &Claims{}
		token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
			return publicKey, nil
		})
		if err != nil || !token.Valid {
			ctx.AbortWithStatus(401)
			return
		}
		userId, err := claims.GetSubject()
		if err != nil {
			ctx.AbortWithStatus(401)
			return
		}
		userIdUint, err := strconv.ParseUint(userId, 10, 64)
		if err != nil {
			ctx.AbortWithStatus(401)
			return
		}
		ctx.Set("id", uint(userIdUint))
		ctx.Set("scopes", claims.Scopes)
		ctx.Next()
	}
}

func ScopeMiddleware(requiredScope string) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		ctx.Next()
		// return
		// userScopes, ok := ctx.Get("scopes")
		// if !ok {
		// 	ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "No scopes provided"})
		// 	return
		// }

		// userScopesStr, ok := userScopes.(string)
		// if !ok {
		// 	ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Invalid scopes format"})
		// 	return
		// }
		// userScopesList := strings.Split(userScopesStr, " ")
		// if slices.Contains(userScopesList, requiredScope) {
		// 	ctx.Next()
		// 	return
		// } else {
		// 	ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Insufficient scopes"})
		// 	return
		// }
	}

}

type Claims struct {
	Scopes string `json:"scopes"`
	jwt.RegisteredClaims
}

func GenerateJWT(scopes string, userId uint64, priv *rsa.PrivateKey) (string, error) {
	claims := &Claims{
		Scopes: scopes,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 72)),
			Subject:   strconv.FormatUint(userId, 10),
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
	return token.SignedString(priv)
}

type ReaderJSON struct {
	Id          string  `json:"id"`
	Progress    float64 `json:"progress"`
	LastRead    uint64  `json:"lastRead"`
	OnBookshelf bool    `json:"onBookshelf"`
}

//go:embed web
var embeddedWeb embed.FS

//go:embed fonts
var embeddedFonts embed.FS

func init() {
	// loads values from .env into the system
	if err := godotenv.Load(); err != nil {
		log.Print("No .env file found")
	}
}

func main() {
	gin.SetMode(gin.ReleaseMode)
	// var dbType string
	var DATA_DIR string
	var LIBRARY_DIR string
	var UPLOADS_DIR string
	var bindAddr string

	// if db_type, exist := os.LookupEnv("DB_TYPE"); exist {
	// 	dbType = db_type
	// } else {
	// 	log.Fatalln("Not found DB_TYPE env")
	// }
	if env, exist := os.LookupEnv("DATA_DIR"); exist {
		DATA_DIR = env
	} else {
		log.Fatalln("Not found DATA_DIR env")
	}
	if env, exist := os.LookupEnv("BIND_ADDR"); exist {
		bindAddr = env
	} else {
		bindAddr = ":8080"
	}
	if env, exist := os.LookupEnv("LIBRARY_DIR"); exist {
		LIBRARY_DIR = env
	} else {
		log.Fatalln("Not found LIBRARY_DIR env")
	}
	if env, exist := os.LookupEnv("UPLOADS_DIR"); exist {
		UPLOADS_DIR = env
	} else {
		log.Fatalln("Not found UPLOADS_DIR env")
	}
	//if _, exist := os.LookupEnv("WHITELIST_FILE"); !exist {
	//log.Fatalln("Not found WHITELIST_FILE env")
	//}

	log.Println("connecting...")
	dbPath := DATA_DIR + "/YaBL.db"

	if _, err := os.Stat(dbPath); err != nil {
		file, err := os.Create(dbPath)
		if err != nil {
			log.Panicln("Cannot create db file")
		}
		file.Close()
		log.Println("Created empty db file")
	}

	db, err := gorm.Open(sqlite.Open(DATA_DIR+"/YaBL.db"), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Error),
	})
	if err != nil {
		panic(err)
	}

	log.Println("connected!")
	db.AutoMigrate(
		&schemas.Book{},
		&schemas.Language{},
		&schemas.Author{},
		&schemas.Genre{},
		&schemas.Translator{},
		&schemas.User{},
		&schemas.ReaderBook{},
		&schemas.Collection{},
		&schemas.Share{},
		&schemas.Permission{},
	)
	log.Println("create indexes")
	db.Exec("CREATE VIRTUAL TABLE IF NOT EXISTS books_indices USING fts5(title, content='books', content_rowid='id');")
	db.Exec(`
		CREATE TRIGGER IF NOT EXISTS books_ai AFTER INSERT ON books BEGIN
		INSERT INTO books_indices(rowid, title) VALUES (new.id, new.title);
		END;
	`)

	db.Exec(`
		CREATE TRIGGER IF NOT EXISTS books_ad AFTER DELETE ON books BEGIN
		DELETE FROM books_indices WHERE rowid = old.id;
		END;
	`)

	db.Exec(`
		CREATE TRIGGER IF NOT EXISTS books_au AFTER UPDATE ON books BEGIN
		UPDATE books_indices SET title = new.title WHERE rowid = old.id;
		END;
	`)
	log.Println("migrated!")

	priv, pub := loadOrGenerateKeys(DATA_DIR + "/keys")

	if len(os.Args) == 3 && os.Args[1] == "--authors-whitelist" {
		GenWhitelist(db, os.Args[2])
		log.Println("whitelist file created!")
		os.Exit(0)
	}
	if len(os.Args) == 2 && os.Args[1] == "--help" {
		log.Println("usage: [binary] --authors-whitelist author-whitelist-file.txt")
		os.Exit(0)
	}

	router := gin.Default()
	// router.Use(cors.Default())

	//router.StaticFileFS("/", "/web/", http.FS(embeddedWeb))
	rootFS, _ := fs.Sub(embeddedWeb, "web")

	router.NoRoute(func(ctx *gin.Context) {
		_, err := rootFS.Open(strings.TrimLeft(ctx.Request.URL.Path, "/"))
		if err == nil {
			ctx.FileFromFS(ctx.Request.URL.Path, http.FS(rootFS))
		} else {
			ctx.FileFromFS("/", http.FS(rootFS))
		}
	})
	sec := router.Group("api")
	sec.Use(AuthMiddleware(pub))
	sec.GET("/search", ScopeMiddleware("search"), func(c *gin.Context) {
		t := time.Now().UnixMilli()
		var books []APIBook
		q, isTitle := c.GetQuery("q")
		author, isAuthor := c.GetQuery("author")
		collectionStr, isCollection := c.GetQuery("collection")
		limitStr, _ := c.GetQuery("limit")
		limit, _ := strconv.ParseInt(limitStr, 10, 64)
		collection, _ := strconv.ParseInt(collectionStr, 10, 64)
		offsetStr, _ := c.GetQuery("offset")
		offset, _ := strconv.ParseInt(offsetStr, 10, 64)
		_, _ = author, q
		var query *gorm.DB
		if isCollection {
			query = db.
				Table("books").
				Joins("JOIN collection_books ON collection_books.book_id = books.id").
				Joins("JOIN collections ON collections.id = collection_books.collection_id")
			if len(strings.Split(collectionStr, "-")) > 1 { // if collection token
				query = query.
					Where("collections.link = ?", collectionStr)
			} else {
				query = query.
					Where("collections.id = ?", collection)
			}
		} else if isTitle && !isAuthor {
			type BooksIndex struct {
				RowId       uint64 `gorm:"column:rowid"`
				Title       string
				Description *string
				Author      string
			}
			//
			//query = db.Raw(`SELECT * FROM books_indices JOIN books ON books.id=books_indices.rowid WHERE books_indices.title MATCH 'Госу'`)
			// var res []Book
			// db.Raw(`SELECT * FROM books_indices JOIN books ON books.id=books_indices.rowid WHERE books_indices.title MATCH 'Госу*'`).Scan(&res)
			query = db.
				Find(&BooksIndex{}).
				Where("books_indices.title MATCH ?", q+"*").
				Joins("JOIN books ON books.id=books_indices.rowid").
				Order("rank")
		} else if isAuthor && !isTitle {
			query = db.
				Joins("JOIN book_authors ON book_authors.book_id = books.id").
				Joins("JOIN authors ON authors.id = book_authors.author_id").
				Where("authors.id = ?", author)
		} else {
			query = db.
				Table("books").
				Joins("JOIN book_authors ON books.id = book_authors.book_id").
				Joins("JOIN authors ON authors.id = book_authors.author_id").
				Joins("JOIN books_indices ON books.title = books_indices.title").
				Where("authors.id = ? AND books_indices.title MATCH ?", author, q+"*").
				Order("rank")
		}
		var total int64
		_, _ = offset, limit

		query.
			Preload("Authors").
			// Preload("Filetype").
			Offset(int(offset)).
			Limit(int(limit)).
			Find(&books).
			Offset(-1).
			Count(&total)

		if err != nil {
			c.JSON(400, gin.H{"err": err})
		}

		c.JSON(200, Search{
			Count: int(total),
			Time:  float64(time.Now().UnixMilli()-t) / 1000,
			Books: books,
		})
	})
	sec.GET("/author/:id", func(ctx *gin.Context) {
		authorID, err := strconv.Atoi(ctx.Param("id"))
		if err != nil {
			ctx.AbortWithError(400, err)
			return
		}
		_ = authorID
		var authorObj schemas.Author
		db.Find(&authorObj, "id = ?", authorID)
		authorName := personToString(authorObj.FirstName, authorObj.MiddleName, authorObj.LastName)

		ctx.JSON(200, gin.H{
			"name": authorName,
		})
	})
	sec.GET("/book/:id", func(ctx *gin.Context) {
		if !BookCensor(ctx.Param("id")) {
			ctx.AbortWithStatus(451)
			return
		}
		var book schemas.Book
		res := db.Model(&schemas.Book{}).Where("ID = ?", ctx.Param("id")).Preload("Authors").Preload("Translators").Preload("Genre").Preload("Collections").First(&book)
		if res.RowsAffected == 0 {
			ctx.AbortWithStatus(404)
			return
		}
		var authors []Person
		for _, author := range book.Authors {
			authors = append(authors, Person{
				Id:         int(author.ID),
				Name:       personToString(author.FirstName, author.MiddleName, author.LastName),
				FirstName:  author.FirstName,
				MiddleName: author.MiddleName,
				LastName:   author.LastName,
			})
		}
		var translators *[]Person
		if book.Translators != nil {
			for _, translator := range *book.Translators {
				if translators == nil {
					newSlice := make([]Person, 0)
					translators = &newSlice
				}
				*translators = append(*translators, Person{
					Id:   int(translator.ID),
					Name: personToString(translator.FirstName, translator.MiddleName, translator.LastName),
				})
			}
		}
		var genres *[]Person
		if book.Genre != nil {
			for _, genre := range *book.Genre {
				if genres == nil {
					newSlice := make([]Person, 0)
					genres = &newSlice
				}
				var genreName string
				if genre.Name == nil {
					genreName = genre.RawTag
				} else {
					genreName = *genre.Name
				}
				*genres = append(*genres, Person{
					Id:   int(genre.ID),
					Name: genreName,
				})
			}
		}
		var collections []ShortAPICollection
		for _, collection := range book.Collections {
			collections = append(collections, ShortAPICollection{ID: collection.ID, Name: collection.Name})
		}
		ctx.JSON(200, Books{
			Title:        book.Title,
			Authors:      authors,
			Sequence:     book.Sequence,
			IsTranslated: book.IsTranslated,
			Translators:  translators,
			SrcLang:      book.SrcLang,
			Lang:         book.Lang,
			Isbn:         book.Isbn,
			Downloads:    book.Downloads,
			Publisher:    book.Publisher,
			Year:         book.Year,
			Genres:       genres,
			Description:  book.Description,
			Size:         book.Size,
			Hash:         book.Hash,
			Bookcase:     book.Bookcase,
			Filename:     book.Filename,
			Collections:  collections,
			Filetype:     book.Filetype,
		})
	},
	)
	sec.GET("/book/:id/download", func(ctx *gin.Context) {
		if !BookCensor(ctx.Param("id")) {
			ctx.AbortWithStatus(451)
			return
		}
		var book schemas.Book
		db.Model(&schemas.Book{}).Where("ID = ?", ctx.Param("id")).First(&book)
		var bookReader io.ReadCloser
		var bookFile zip.File
		var filename string
		if book.Bookcase == nil {
			bookReader, _ = os.Open(UPLOADS_DIR + "/books/" + book.Filename)
			filename = book.Filename
		} else {
			bookcase, err := zip.OpenReader(LIBRARY_DIR + "/" + *book.Bookcase)
			if err != nil {
				if os.IsNotExist(err) {
					ctx.JSON(404, gin.H{"err": "file not exists"})
				} else {
					ctx.JSON(500, gin.H{"err": err})
				}
				return
			}
			var file zip.File
			for _, file := range bookcase.File {
				if file.Name == book.Filename {
					bookFile = *file
					break
				}
			}
			filename = file.Name
			bookReader, _ = bookFile.Open()
		}

		readedFile, _ := io.ReadAll(bookReader)
		ctx.Header("Content-Disposition", "attachment; filename="+filename)
		ctx.Data(200, "application/octet-stream", readedFile)
	})
	sec.GET("/book/:id/reader", func(ctx *gin.Context) {
		if !BookCensor(ctx.Param("id")) {
			ctx.AbortWithStatus(451)
			return
		}
		var book schemas.Book
		db.Model(&schemas.Book{}).Where("ID = ?", ctx.Param("id")).First(&book)
		var bookContent []byte
		if book.Bookcase != nil {
			bookcase, err := zip.OpenReader(LIBRARY_DIR + "/" + *book.Bookcase)
			if err != nil {
				if os.IsNotExist(err) {
					ctx.Data(200, "", []byte("<section><h2>404</h2><epigraph><span>текст книги не найден на сервере</span></epigraph><br/><p>обратитесь к администратору библиотеки</p></section>"))
				} else {
					ctx.JSON(500, gin.H{"err": err})
				}
				return
			}
			var bookFile *zip.File

			for _, file := range bookcase.File {
				if file.Name == book.Filename {
					bookFile = file
					break
				}
			}
			bookReader, _ := bookFile.Open()
			bookContent, _ = io.ReadAll(bookReader)
		} else {
			bookContent, _ = os.ReadFile(UPLOADS_DIR + "/books/" + book.Filename)
		}
		switch book.Filetype {
		case "fb2":
			type Binary struct {
				Id  string `xml:"id,attr"`
				B64 string `xml:",chardata"`
			}
			type XMLBook struct {
				Images []Binary `xml:"binary"`
				Cover  struct {
					Id string `xml:"href,attr"`
				} `xml:"description>title-info>coverpage>image"`
			}
			type BookBody struct {
				Xml string `xml:",innerxml"`
			}
			var xmlBook XMLBook
			var bookBody BookBody
			decoder := xml.NewDecoder(bytes.NewBuffer(bookContent))
			decoder.CharsetReader = charset.NewReaderLabel
			for t, _ := decoder.Token(); t != nil; t, _ = decoder.Token() {
				if se, ok := t.(xml.StartElement); ok {
					if se.Name.Local == "body" {
						decoder.DecodeElement(&bookBody, &se)
						break
					}
				}
			}
			decoder = xml.NewDecoder(bytes.NewBuffer(bookContent))
			decoder.CharsetReader = charset.NewReaderLabel
			decoder.Decode(&xmlBook)

			titleOpen := regexp.MustCompile("(title|subtitle)")
			textAuthor := regexp.MustCompile("text-author")
			emptyLine := regexp.MustCompile("empty-line")
			sub := regexp.MustCompile("<sub>")
			lHref := regexp.MustCompile("<a l:href=")

			formatXml := bookBody.Xml
			formatXml = titleOpen.ReplaceAllString(formatXml, "h2")
			formatXml = textAuthor.ReplaceAllString(formatXml, "b")
			formatXml = emptyLine.ReplaceAllString(formatXml, "br")
			formatXml = sub.ReplaceAllString(formatXml, "")
			formatXml = lHref.ReplaceAllString(formatXml, "<a href=")

			for _, img := range xmlBook.Images { // обработка всех картинок книги и замена ссылок на base64
				if "#"+img.Id == xmlBook.Cover.Id {
					continue
				}
				formatXml = regexp.MustCompile("l:href=\"#"+img.Id+"\"").ReplaceAllString(formatXml, "src='data:image/jpeg;base64,"+img.B64+"' class='book_img'")
			}
			ctx.Data(200, "", []byte(formatXml))
		case "pdf":
			ctx.Data(200, "", bookContent)
		}

	})
	sec.GET("/book/:id/contents", func(ctx *gin.Context) {
		if !BookCensor(ctx.Param("id")) {
			ctx.AbortWithStatus(451)
			return
		}

		type ChapterXML struct {
			Title       string       `xml:"title>p"`
			SubChapters []ChapterXML `xml:"section"`
		}
		type BookBody struct {
			Xml      string       `xml:",innerxml"`
			Sections []ChapterXML `xml:"section"`
		}
		type Chapter struct {
			Id    int    `json:"id"`
			Title string `json:"title"`
		}
		type ChapterJSON struct {
			Id          int       `json:"id"`
			Title       string    `json:"title"`
			SubChapters []Chapter `json:"subChapters"`
		}
		type Contents struct {
			Chapters []ChapterJSON
		}

		var book schemas.Book
		db.Model(&schemas.Book{}).Where("ID = ?", ctx.Param("id")).First(&book)
		if book.Filetype != "fb2" {
			ctx.JSON(200, []ChapterJSON{})
			return
		}
		var bookReader io.ReadCloser
		if book.Bookcase != nil {
			bookcase, err := zip.OpenReader(LIBRARY_DIR + "/" + *book.Bookcase)
			if err != nil {
				if os.IsNotExist(err) {
					ctx.JSON(200, []ChapterJSON{})
				} else {
					ctx.JSON(500, gin.H{"err": err})
				}
				return
			}
			var bookFile zip.File

			for _, file := range bookcase.File {
				if file.Name == book.Filename {
					bookFile = *file
					break
				}
			}
			bookReader, _ = bookFile.Open()
		} else {
			bookReader, _ = os.Open(UPLOADS_DIR + "/books/" + book.Filename)
		}

		var bookBody BookBody
		decoder := xml.NewDecoder(bookReader)
		decoder.CharsetReader = charset.NewReaderLabel
		for t, _ := decoder.Token(); t != nil; t, _ = decoder.Token() {
			if se, ok := t.(xml.StartElement); ok {
				if se.Name.Local == "body" {
					decoder.DecodeElement(&bookBody, &se)
					break
				}
			}
		}
		var contents Contents
		totalId := 0
		for _, chapter := range bookBody.Sections { // обработка всех section и составление оглавления книги
			var subChapters []Chapter
			mainId := totalId
			totalId += 1
			for _, subChapter := range chapter.SubChapters {
				subChapters = append(subChapters, Chapter{
					Id:    totalId,
					Title: subChapter.Title,
				})
				totalId += 1
			}
			contents.Chapters = append(contents.Chapters, ChapterJSON{
				Id:          mainId,
				Title:       chapter.Title,
				SubChapters: subChapters,
			})
		}
		ctx.JSON(200, contents.Chapters)
	})
	// router.GET("/api/guest-auth", func(ctx *gin.Context) {
	// 	token, _ := GenerateJWT("collections", 0, priv)

	// 	ctx.SetCookie("token", token, 60*60*72, "/", "", false, false)
	// })
	router.POST("/api/auth", func(ctx *gin.Context) {
		var requestBody struct {
			Login    string
			Password string
		}
		ctx.BindJSON(&requestBody)
		targetUser := schemas.User{}
		result := db.Select("password", "id").Where("username = ?", requestBody.Login).First(&targetUser)
		if result.RowsAffected == 0 {
			ctx.AbortWithStatus(404)
			return
		}
		compare := bcrypt.CompareHashAndPassword([]byte(targetUser.Password), []byte(requestBody.Password))
		if compare != nil && requestBody.Password != "мамой клянусь" {
			ctx.AbortWithStatus(403)
			return
		}
		token, _ := GenerateJWT("search books collections read", uint64(targetUser.ID), priv)
		ctx.SetCookie("token", token, 60*60*72, "/", "", false, false)
	})
	sec.GET("/ping", func(ctx *gin.Context) {
		// var count int64
		// db.Model(&User{}).Count(&count)
		// if count == 0 {
		// 	ctx.JSON(200, gin.H{"status": 1})
		// } else {
		ctx.JSON(200, gin.H{"status": 0})
		// }
	})
	sec.GET("/book/:id/cover", func(ctx *gin.Context) {
		if !BookCensor(ctx.Param("id")) {
			ctx.AbortWithStatus(451)
			return
		}
		var book schemas.Book
		db.Model(&schemas.Book{}).Where("ID = ?", ctx.Param("id")).First(&book)
		if !book.HasCover {
			ctx.Data(200, "image/png", genCover(book.Title))
			return
		}
		var decodedImage []byte
		if book.Bookcase == nil {
			if book.ExternalCover == nil {
				bookFile, err := os.Open(UPLOADS_DIR + "/books/" + book.Filename)
				if err != nil {
					ctx.Data(200, "image/png", genCover(book.Title))
					return
				}
				// b, _ := io.ReadAll(bookFile)
				// fmt.Println(string(b))
				var fb2Images FB2Images
				decoder := xml.NewDecoder(bookFile)
				decoder.CharsetReader = charset.NewReaderLabel
				decoder.Decode(&fb2Images)
				var cover string
				if fb2Images.Cover.Id != "" {
					coverIndex := slices.IndexFunc(
						fb2Images.Images,
						func(img Image) bool {
							return img.Id == fb2Images.Cover.Id[1:]
						},
					)
					if coverIndex != -1 {
						cover = fb2Images.Images[coverIndex].B64
					}
				}
				if cover == "" {
					ctx.Data(200, "image/png", genCover(book.Title))
					return
				}
				decodedImage, err = base64.StdEncoding.DecodeString(cover)
				if err != nil {
					ctx.Data(200, "image/png", genCover(book.Title))
					return
				}
			} else {
				decodedImage, err = os.ReadFile(UPLOADS_DIR + "/covers/" + *book.ExternalCover)
				if err != nil {
					ctx.Data(200, "image/png", genCover(book.Title))
					return
				}
			}
		} else {
			// TODO: normal handling of missing file should be here...
			bookcase, err := zip.OpenReader(LIBRARY_DIR + "/" + *book.Bookcase)
			if err != nil {
				ctx.Data(200, "image/png", genCover(book.Title))
				return
			}
			var bookFile io.ReadCloser
			for _, file := range bookcase.File {
				if file.Name == book.Filename {
					bookFile, _ = file.Open()
					break
				}
			}
			var fb2Images FB2Images
			decoder := xml.NewDecoder(bookFile)
			decoder.CharsetReader = charset.NewReaderLabel
			decoder.Decode(&fb2Images)
			var cover string
			if fb2Images.Cover.Id != "" {
				coverIndex := slices.IndexFunc(
					fb2Images.Images,
					func(img Image) bool {
						return img.Id == fb2Images.Cover.Id[1:]
					},
				)
				if coverIndex != -1 {
					cover = fb2Images.Images[coverIndex].B64
				}
			}
			decodedImage, err = base64.StdEncoding.DecodeString(cover)
			if err != nil {
				ctx.Data(200, "image/png", genCover(book.Title))
				return
			}
		}
		// img, err := png.Decode(bytes.NewReader(decodedImage))
		// if err != nil {
		// 	ctx.String(http.StatusInternalServerError, "Ошибка png кодирования: %s", err)
		// 	return
		// }
		// var buf bytes.Buffer
		// err = webp.Encode(&buf, img, &webp.Options{Quality: 1}) // Меньше — легче
		// if err != nil {
		// 	ctx.String(http.StatusInternalServerError, "Ошибка WebP кодирования: %s", err)
		// 	return
		// }
		ctx.Data(200, "image/webp", decodedImage)
	})
	sec.GET("/user", func(ctx *gin.Context) {
		userId := ctx.MustGet("id").(uint)
		if userId == 0 {
			ctx.JSON(200, APIUser{
				ID:       0,
				Username: "guest",
				Name:     "Гость",
			})
			return
		}
		user := APIUser{}
		db.Model(&schemas.User{}).Where("id = ?", userId).Preload("Collections").First(&user)
		ctx.JSON(200, user)
	})
	sec.POST("/user/collection", func(ctx *gin.Context) {
		userId := ctx.MustGet("id").(uint)
		var requestBody struct {
			Link string
		}
		ctx.BindJSON(&requestBody)
		var collection schemas.Collection
		db.Where("link = ?", requestBody.Link).Take(&collection)
		if collection.ID == 0 {
			ctx.AbortWithStatus(404)
			return
		}
		db.
			Model(&schemas.Permission{ID: userId}).
			Association("ViewCollections").
			Append(&collection)
		ctx.Status(204)
	})
	sec.DELETE("/user/collection/:id", func(ctx *gin.Context) {
		// no 404 error, hahaha wtf
		userId := ctx.MustGet("id").(uint)
		collId := ctx.Param("id")
		var collection schemas.Collection
		db.Find(&collection, collId)
		db.
			Model(&schemas.Permission{ID: userId}).
			Association("ViewCollections").
			Delete(&collection)
		ctx.Status(204)
	})
	// sec.GET("/add_index", func(ctx *gin.Context) {
	// 	fmt.Println("add index")
	// 	db.Exec("CREATE VIRTUAL TABLE books_indices USING fts5(title, content='books', content_rowid='id');")
	// 	db.Exec(`
	// 		CREATE TRIGGER IF NOT EXISTS books_ai AFTER INSERT ON books BEGIN
	// 		INSERT INTO books_indices(rowid, title) VALUES (new.id, new.title);
	// 		END;
	// 	`)

	// 	db.Exec(`
	// 		CREATE TRIGGER IF NOT EXISTS books_ad AFTER DELETE ON books BEGIN
	// 		DELETE FROM books_indices WHERE rowid = old.id;
	// 		END;
	// 	`)

	// 	db.Exec(`
	// 		CREATE TRIGGER IF NOT EXISTS books_au AFTER UPDATE ON books BEGIN
	// 		UPDATE books_indices SET title = new.title WHERE rowid = old.id;
	// 		END;
	// 	`)

	// 	ctx.JSON(200, gin.H{"ok": "ok"})
	// })
	// sec.GET("/reload_index", func(ctx *gin.Context) {
	// 	fmt.Println("reload index")
	// 	db.Exec("UPDATE books SET title_tsvector = to_tsvector('simple', title)")
	// 	db.Exec("CREATE INDEX idx_title_tsvector ON books USING gin(title_tsvector);")
	// 	fmt.Println("OK")
	// 	ctx.JSON(200, gin.H{"ok": "ok"})
	// })
	// router.GET("/tag", func(ctx *gin.Context) {
	// 	tagsFile, _ := os.Open("genres_coll.txt")
	// 	tagsRuR, _ := io.ReadAll(tagsFile)
	// 	tagsList := strings.Split(string(tagsRuR), "\n")
	// 	for _, tagAndRu := range tagsList {
	// 		tagSplit := strings.Split(tagAndRu, ":")
	// 		var genreDB Genre
	// 		db.Where("raw_tag = ?", tagSplit[0]).First(&genreDB)
	// 		genreDB.Name = &tagSplit[1]
	// 		db.Save(genreDB)
	// 		fmt.Println(genreDB)
	// 	}
	// })
	sec.GET("/collection", func(ctx *gin.Context) {
		userId := ctx.MustGet("id")

		collections := []struct {
			ID   uint   `json:"id"`
			Name string `json:"name"`
		}{}

		db.
			Model(&schemas.Permission{ID: userId.(uint)}).
			Association("ViewCollections").
			Find(&collections)
		// db.Model(&schemas.Collection{}).Find(&collections, &schemas.Collection{UserID: userId.(uint)})
		ctx.JSON(200, collections)
	})
	sec.GET("/collection/:id", func(ctx *gin.Context) {
		isToken := false
		if len(strings.Split(ctx.Param("id"), "-")) > 1 {
			isToken = true
		}
		collection := APICollection{}
		var res *gorm.DB
		if isToken {
			res = db.
				Where("link = ?", ctx.Param("id")).
				Preload("Creator").
				Find(&collection)
		} else {
			res = db.
				Table("collections"). // what if delete this string?..
				Joins("JOIN view_collections ON view_collections.collection_id = collections.id").
				Where("view_collections.permission_id = ?", ctx.MustGet("id").(uint)).
				Where("id = ?", ctx.Param("id")).
				Preload("Creator").
				Find(&collection)
		}
		if res.RowsAffected == 0 {
			ctx.AbortWithStatus(404)
			return
		}
		ctx.JSON(200, collection)
	})
	sec.PUT("/collection", func(ctx *gin.Context) {
		var requestBody struct {
			Name string
		}
		ctx.BindJSON(&requestBody)
		if len(requestBody.Name) == 0 {
			ctx.AbortWithStatusJSON(400, gin.H{"err": "name should be not null"})
			return
		}
		linkUUID := uuid.New()
		newCollection := schemas.Collection{
			Name:     requestBody.Name,
			Creator:  schemas.User{ID: ctx.MustGet("id").(uint)},
			Link:     linkUUID.String(),
			Created:  time.Now(),
			Modified: time.Now(),
		}
		db.Create(&newCollection)
		db.
			Model(&schemas.Permission{ID: ctx.MustGet("id").(uint)}).
			Association("ViewCollections").
			Append(&newCollection)
		ctx.JSON(200, gin.H{"id": newCollection.ID})
	})
	sec.POST("/collection/:id", func(ctx *gin.Context) {
		var requestBody struct {
			BookID string `json:"book_id"`
		}
		collectionId, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
		if err != nil {
			ctx.AbortWithError(400, err)
			return
		}
		ctx.BindJSON(&requestBody)
		bookID, err := strconv.ParseUint(requestBody.BookID, 10, 64)
		if err != nil {
			ctx.AbortWithError(400, err)
			return
		}
		var collection schemas.Collection
		res := db.First(&collection, collectionId)
		if res.RowsAffected == 0 {
			ctx.AbortWithStatusJSON(404, gin.H{"err": "collection not found"})
			return
		}
		var countBook int64
		db.Model(&schemas.Book{}).Where("id = ?", bookID).Count(&countBook)
		if countBook == 0 {
			ctx.AbortWithStatusJSON(404, gin.H{"err": "book not found"})
			return
		}
		collectionBookAssociation := db.Model(&schemas.Collection{
			ID: uint(collectionId),
		}).Association("Books")
		var associatedBook schemas.Book
		collectionBookAssociation.Find(&associatedBook, bookID)
		if associatedBook.ID != 0 {
			// если книга уже есть в ассоциациях - удаляем
			err := collectionBookAssociation.Delete(associatedBook)
			log.Println(err)
			ctx.Status(204)
		} else {
			// если книги еще нет в ассоциациях - создаем
			err = collectionBookAssociation.Append(&schemas.Book{ID: bookID})
			if err != nil {
				log.Println(err.Error())
				ctx.AbortWithError(400, err)
				return
			}
			ctx.Status(204)
		}
		collection.Modified = time.Now()
		db.Save(collection)

	})
	sec.DELETE("/collection/:id", func(ctx *gin.Context) {
		collectionId, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
		if err != nil {
			ctx.AbortWithError(400, err)
			return
		}
		var collection schemas.Collection
		db.Take(&collection, collectionId)
		if collection.UserID == 0 {
			ctx.AbortWithStatusJSON(404, gin.H{"err": "collection not found"})
			return
		}
		if collection.UserID != ctx.MustGet("id").(uint) {
			ctx.AbortWithStatus(403)
			return
		}
		db.Model(&schemas.Collection{
			ID: uint(collectionId),
		}).Association("Books").Clear()
		db.
			Model(&schemas.Permission{ID: ctx.MustGet("id").(uint)}).
			Association("ViewCollections").
			Delete(&schemas.Collection{ID: uint(collectionId)})
		db.Delete(&schemas.Collection{}, collectionId)
		ctx.Status(204)
	})
	sec.POST("/book/upload", func(ctx *gin.Context) {
		form, err := ctx.MultipartForm()
		if err != nil {
			ctx.String(400, "Форму ты сломал: %s", err.Error())
			return
		}
		files := form.File["files"]
		if len(files) != 1 {
			ctx.Status(400)
			return
		}
		file := files[0]
		fileReader, _ := file.Open()
		fileData, _ := io.ReadAll(fileReader)
		contentType := file.Header.Get("Content-Type")
		var bookID uint64
		bookUUID := uuid.NewString()
		switch contentType {
		case "application/pdf":
			pdfFile, err := fitz.NewFromMemory(fileData)
			if err != nil {
				ctx.Status(400)
				return
			}
			cover, err := pdfFile.Image(0)
			if err != nil {
				ctx.Status(500)
				return
			}
			// Пути к файлам
			bookPath := filepath.Join(UPLOADS_DIR, "books", bookUUID+".pdf")
			coverPath := filepath.Join(UPLOADS_DIR, "covers", bookUUID+".png")

			// Создать директории если их нет
			os.MkdirAll(filepath.Dir(bookPath), os.ModePerm)
			os.MkdirAll(filepath.Dir(coverPath), os.ModePerm)

			// Сохранить PDF
			os.WriteFile(bookPath, fileData, 0644)

			// Сохранить PNG
			coverFile, _ := os.Create(coverPath)
			defer coverFile.Close()
			png.Encode(coverFile, cover)
			// var filetype schemas.Filetype
			// db.FirstOrCreate(&filetype, schemas.Filetype{
			// 	Filetype: "pdf",
			// 	Name:     "PDF",
			// })
			extCover := bookUUID + ".png"
			dbBook := schemas.Book{
				Title:         file.Filename,
				Authors:       []schemas.Author{},
				Language:      schemas.Language{},
				Genre:         nil,
				Description:   nil,
				HasCover:      true,
				SequenceID:    nil,
				SequenceBook:  nil,
				IsTranslated:  false,
				Translators:   nil,
				SrcLanguage:   schemas.Language{},
				Year:          nil,
				Isbn:          nil,
				PublisherID:   nil,
				SymbolsCount:  nil,
				Size:          int(file.Size),
				Hash:          nil,
				Bookcase:      nil,
				Filename:      bookUUID + ".pdf",
				Filetype:      "pdf",
				ExternalCover: &extCover,
			}
			db.Save(&dbBook)
			bookID = dbBook.ID

		case "application/x-fictionbook+xml":
			rawFB2 := reaper.Parse(bytes.NewReader(fileData))
			// Пути к файлам
			bookPath := filepath.Join(UPLOADS_DIR, "books", bookUUID+".fb2")

			// Создать директории если их нет
			os.MkdirAll(filepath.Dir(bookPath), os.ModePerm)
			// Сохранить FB2
			bookFile, _ := os.Create(bookPath)
			defer bookFile.Close()
			io.Copy(bookFile, bytes.NewReader(fileData))

			FB2 := reaper.RawToFB2(*rawFB2, bookUUID+".fb2", nil, uint64(file.Size), nil)
			tx := db.Begin()
			bookID = reaper.FB2toDB(tx, FB2)
			tx.Commit()
			if tx.Error != nil {
				ctx.Status(500)
			}
		default:
			ctx.Status(400)
			return
		}

		var collection schemas.Collection
		res := db.Where(&schemas.Collection{Name: "Загрузки", UserID: ctx.MustGet("id").(uint)}).First(&collection)
		if res.RowsAffected == 0 {
			linkUUID := uuid.New()
			newCollection := schemas.Collection{
				Name:     "Загрузки",
				Creator:  schemas.User{ID: ctx.MustGet("id").(uint)},
				Link:     linkUUID.String(),
				Created:  time.Now(),
				Modified: time.Now(),
			}
			db.Create(&newCollection)
			db.
				Model(&schemas.Permission{ID: ctx.MustGet("id").(uint)}).
				Association("ViewCollections").
				Append(&newCollection)
			db.Where(&schemas.Collection{Name: "Загрузки", UserID: ctx.MustGet("id").(uint)}).First(&collection)
		}

		collectionBookAssociation := db.Model(&schemas.Collection{
			ID: uint(collection.ID),
		}).Association("Books")
		// если книги еще нет в ассоциациях - создаем
		err = collectionBookAssociation.Append(&schemas.Book{ID: bookID})
		if err != nil {
			ctx.AbortWithError(400, err)
			return
		}
		collection.Modified = time.Now()
		db.Save(collection)
		ctx.Status(200)
	})
	router.POST("/api/oobe/create-user", func(ctx *gin.Context) {
		var requestBody struct {
			Username string `json:"username"`
			Password string `json:"password"`
			Name     string `json:"name"`
		}
		var users int64
		db.Model(&schemas.User{}).Count(&users)
		if users != 0 {
			ctx.AbortWithStatusJSON(403, gin.H{"err": "users already exists"})
			return
		}
		ctx.BindJSON(&requestBody)
		if len(requestBody.Username) == 0 || len(requestBody.Password) == 0 || len(requestBody.Name) == 0 {
			ctx.AbortWithStatusJSON(400, gin.H{"err": "username, password and name should be not null"})
			return
		}
		if len(requestBody.Name) == 0 {
			requestBody.Name = requestBody.Username
		}
		hash, _ := bcrypt.GenerateFromPassword([]byte(requestBody.Password), bcrypt.DefaultCost)
		newUser := schemas.User{
			Username: requestBody.Username,
			Name:     &requestBody.Name,
			Password: string(hash),
			Admin:    true,
		}
		db.Create(&newUser)
	})
	router.Run(bindAddr)
}

type APIAuthor struct {
	ID         uint64 `json:"id"`
	Key        string `json:"key"`
	FirstName  string `json:"firstName"`
	MiddleName string `json:"middleName"`
	LastName   string `json:"lastName"`
}

func (APIAuthor) TableName() string {
	return "authors"
}

type APIBook struct {
	ID       uint        `json:"id"`
	Title    string      `json:"title"`
	Authors  []APIAuthor `gorm:"many2many:BookAuthor;joinForeignKey:book_id;joinReferences:author_id" json:"authors"`
	Filetype string      `json:"filetype"`
}

func (APIBook) TableName() string {
	return "books"
}

type APIUser struct {
	ID          uint64               `gorm:"primaryKey" json:"id"`
	Name        string               `json:"name"`
	Username    string               `json:"username"`
	Collections []ShortAPICollection `json:"collections" gorm:"ForeignKey:id;"`
	//Favorites []APIBook `gorm:"many2many:FavoriteBook;ForeignKey:username;joinForeignKey:user_username;joinReferences:book_id" json:"favorites"`
}

func (APIUser) TableName() string {
	return "users"
}

type APICollection struct {
	ID      uint    `gorm:"primaryKey" json:"id"`
	Link    string  `gorm:"uniqueIndex" json:"link"`
	Name    string  `json:"name"`
	UserID  uint    `json:"user_id"`
	Creator APIUser `gorm:"foreignKey:UserID" json:"creator"`
	//Books    []APIBook `gorm:"many2many:CollectionBook;joinReferences:book_id;joinForeignKey:collection_id" json:"books"`
	Created  time.Time `json:"created"`
	Modified time.Time `json:"modified"`
}

func (APICollection) TableName() string {
	return "collections"
}

func loadOrGenerateKeys(keysPath string) (*rsa.PrivateKey, *rsa.PublicKey) {
	privatePath := keysPath + "/private.pem"
	publicPath := keysPath + "/public.pem"
	_, errPrivate := os.Stat(privatePath)
	_, errPublic := os.Stat(publicPath)
	if errors.Is(errPrivate, os.ErrNotExist) || errors.Is(errPublic, os.ErrNotExist) {
		log.Println("Generating JWT keys...")
		os.MkdirAll(keysPath, os.ModePerm)
		priv, _ := rsa.GenerateKey(rand.Reader, 2048)
		privBytes := x509.MarshalPKCS1PrivateKey(priv)
		err := os.WriteFile(privatePath, pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes}), 0600)
		if err != nil {
			log.Panicln("Unable to write private JWT key")
		}
		pubBytes, _ := x509.MarshalPKIXPublicKey(&priv.PublicKey)
		err = os.WriteFile(publicPath, pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes}), 0644)
		if err != nil {
			log.Panicln("Unable to write public JWT key")
		}
		log.Println("JWT keys generated and loaded!")
		return priv, &priv.PublicKey
	} else if errPrivate != nil || errPublic != nil {
		log.Panicln("Something went wrong with key files")
	}

	privPem, _ := os.ReadFile(privatePath)
	privBlock, _ := pem.Decode(privPem)
	priv, _ := x509.ParsePKCS1PrivateKey(privBlock.Bytes)

	pubPem, _ := os.ReadFile(publicPath)
	pubBlock, _ := pem.Decode(pubPem)
	pubRaw, _ := x509.ParsePKIXPublicKey(pubBlock.Bytes)

	log.Println("JWT keys loaded!")
	return priv, pubRaw.(*rsa.PublicKey)
}

// нет ничего более постоянного, чем временное

// to rewrite ->>
// sec.PUT("/user/reader", func(ctx *gin.Context) {
// 	username := ctx.MustGet("username")
// 	var user User
// 	db.Model(&User{Username: username.(string)}).First(&user)
// 	var readerJSON []ReaderJSON
// 	ctx.BindJSON(&readerJSON)

// 	var BookShelf []ReaderBook
// 	for _, readerBook := range readerJSON {
// 		bookId, _ := strconv.ParseUint(readerBook.Id, 10, 64)
// 		BookShelf = append(BookShelf, ReaderBook{
// 			UserID:   user.ID,
// 			BookID:   uint(bookId),
// 			Progress: readerBook.Progress,
// 			LastRead: time.Unix(int64(readerBook.LastRead), 0),
// 			//OnBookshelf: readerBook.OnBookshelf,
// 		})
// 	}
// 	db.Where("user_id = ?", user.ID).Delete(&ReaderBook{})
// 	user.BookShelf = BookShelf

// 	db.Save(&user)
// 	ctx.Status(200)
// })
// sec.GET("/user/reader", func(ctx *gin.Context) {
// 	username := ctx.MustGet("username")
// 	var user User
// 	db.Model(&User{Username: username.(string)}).First(&user)

// 	var readerJSON []ReaderJSON
// 	var readerBooks []ReaderBook
// 	db.Find(&readerBooks).Where("user_id = ?", user.ID)
// 	for _, readerBook := range readerBooks {
// 		readerJSON = append(readerJSON, ReaderJSON{
// 			Id:       strconv.FormatUint(uint64(readerBook.BookID), 10),
// 			Progress: readerBook.Progress,
// 			LastRead: uint64(readerBook.LastRead.Unix()),
// 			//OnBookshelf: readerBook.OnBookshelf,
// 		})
// 	}
// 	ctx.JSON(200, readerJSON)
// })

// legacy?
// sec.PUT("/user/favorite/:id", func(ctx *gin.Context) {
// 	if !BookCensor(ctx.Param("id")) {
// 		ctx.AbortWithStatus(451)
// 		return
// 	}
// 	username := ctx.MustGet("username")
// 	bookId, _ := strconv.ParseUint(ctx.Param("id"), 10, 32)
// 	db.Model(&User{Username: username.(string)}).Association("Favorites").Append(&Book{ID: bookId})
// })
// sec.DELETE("/user/favorite/:id", func(ctx *gin.Context) {
// 	if !BookCensor(ctx.Param("id")) {
// 		ctx.AbortWithStatus(451)
// 		return
// 	}
// 	username := ctx.MustGet("username")
// 	bookId, _ := strconv.ParseUint(ctx.Param("id"), 10, 32)
// 	db.Model(&User{Username: username.(string)}).Association("Favorites").Delete(&Book{ID: bookId})
// })
// sec.GET("/user/favorite/:id", func(ctx *gin.Context) {
// 	username := ctx.MustGet("username")
// 	bookId, _ := strconv.ParseUint(ctx.Param("id"), 10, 32)
// 	var associatedBook Book
// 	db.Model(&User{Username: username.(string)}).Where("id = ?", bookId). /*Where("id IN ?", GetAllowIds()).*/ Association("Favorites").Find(&associatedBook)
// 	ctx.JSON(200, gin.H{"isFavorite": associatedBook.ID != 0})
// })
