package main

import (
	"archive/tar"
	"backend/md"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"
)

type ArticleStatus = int

const (
	ArticleStatusDraft ArticleStatus = iota
	ArticleStatusPublished
	ArticleStatusOffline
)

type ArticleProperties struct {
	Id               int64         `json:"id"`
	Title            string        `json:"title"`
	Status           ArticleStatus `json:"status"`
	Tags             []string      `json:"tags"`
	ReleaseDate      time.Time     `json:"date"`
	ModificationDate *time.Time    `json:"mod-date"`
}

type Article struct {
	ArticleProperties
	Content string        `json:"content"`
	Files   []ArticleFile `json:"-"`
}

type ArticleFile struct {
	Id   int64
	Data []byte
}

const maxArticleFormSize = 10 * 1024 * 1024

func ParseArticle(reader io.Reader, filePrefix string) (*Article, error) {
	tarFiles := make(map[string][]byte)
	tarReader := tar.NewReader(reader)
	for {
		header, err := tarReader.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, err
		}

		content, err := io.ReadAll(tarReader)
		if err != nil {
			return nil, err
		}
		tarFiles[header.Name] = content
	}

	readmeBytes, found := tarFiles["README.md"]
	if !found {
		return nil, errors.New("file 'README.md' not found")
	}

	usedFiles := make(map[string]ArticleFile)
	prevFileId := int64(0)

	pc, html, err := md.Parse(func(path string) (string, error) {
		content, ok := tarFiles[strings.TrimPrefix(path, "./")]
		if !ok {
			return "", errors.New("file '" + path + "' not found")
		}

		prevFileId++
		usedFiles[path] = ArticleFile{prevFileId, content}

		return filePrefix + strconv.FormatInt(prevFileId, 10), nil
	}, readmeBytes)
	if err != nil {
		return nil, err
	}

	title, err := md.GetProperty(pc, "title")
	if err != nil {
		if errors.Is(err, md.PropertyNotFoundError) {
			return nil, errors.New("title property not found")
		}

		return nil, err
	}

	tagsStr, err := md.GetProperty(pc, "tags")
	if err != nil && !errors.Is(err, md.PropertyNotFoundError) {
		return nil, err
	}

	tags := make([]string, 0)
	if strings.TrimSpace(tagsStr) != "" {
		for _, tagStr := range strings.Split(tagsStr, ",") {
			tags = append(tags, strings.TrimSpace(tagStr))
		}
	}

	dateStr, err := md.GetProperty(pc, "date")
	if err != nil {
		if errors.Is(err, md.PropertyNotFoundError) {
			return nil, errors.New("date property not found")
		}

		return nil, err
	}
	releaseDate, err := time.Parse(time.DateOnly, dateStr)
	if err != nil {
		return nil, fmt.Errorf("invalid date property '%s': %v", dateStr, err)
	}

	var modificationDate *time.Time
	dateStr, err = md.GetProperty(pc, "mod-date")
	if err == nil {
		tmp, err := time.Parse(time.DateOnly, dateStr)
		if err != nil {
			return nil, fmt.Errorf("invalid mod-date property '%s': %v", dateStr, err)
		}

		modificationDate = &tmp
	} else if !errors.Is(err, md.PropertyNotFoundError) {
		return nil, err
	}

	files := make([]ArticleFile, 0, len(usedFiles))
	for _, file := range usedFiles {
		files = append(files, file)
	}

	return &Article{
		ArticleProperties{
			-1,
			title,
			ArticleStatusDraft,
			tags,
			releaseDate,
			modificationDate,
		},
		string(html),
		files,
	}, nil
}

func (h *ApiHandler) ServeBlogGet(writer http.ResponseWriter, request *http.Request) {
	query := request.URL.Query()

	var err error
	var offset int
	var limit int
	tags := make([]string, 0)

	offsetStr := query.Get("offset")
	if offsetStr == "" {
		offset = 0
	} else {
		offset, err = strconv.Atoi(offsetStr)
		if err != nil || offset < 0 {
			WriteError(writer, http.StatusBadRequest, "invalid offset", nil)
			return
		}
	}

	limitStr := query.Get("limit")
	if limitStr == "" {
		limit = 50
	} else {
		limit, err = strconv.Atoi(limitStr)
		if err != nil || limit <= 0 || limit > 100 {
			WriteError(writer, http.StatusBadRequest, "invalid limit", nil)
			return
		}
	}

	tagsStr := query.Get("tags")
	if tagsStr != "" {
		tags = strings.Split(tagsStr, ",")
	}

	articles, total, err := h.db.GetBlogArticles(IsAuthorized(request), offset, limit, tags)
	if err != nil {
		WriteError(writer, http.StatusInternalServerError, "failed to query database", err)
		return
	}

	WriteResponse(writer, http.StatusOK, map[string]interface{}{
		"articles": articles,
		"total":    total,
	})
}

func (h *ApiHandler) ServeBlogPut(writer http.ResponseWriter, request *http.Request) {
	err := request.ParseMultipartForm(maxArticleFormSize)
	if err != nil {
		WriteError(writer, http.StatusBadRequest, "failed to parse multipart form", nil)
		return
	}

	file, _, err := request.FormFile("file")
	if err != nil {
		WriteError(writer, http.StatusBadRequest, "failed to parse file", nil)
		return
	}
	defer func() {
		_ = file.Close()
	}()

	id, commit, err := h.db.CreateBlogArticle()
	if err != nil {
		WriteError(writer, http.StatusInternalServerError, "failed to write database", err)
		return
	}

	article, err := ParseArticle(file, "/api/blog/"+strconv.FormatInt(id, 10)+"/file/")
	if err != nil {
		_ = commit(nil)
		WriteError(writer, http.StatusInternalServerError, "failed to parse article: "+err.Error(), nil)
		return
	}

	err = commit(article)
	if err != nil {
		WriteError(writer, http.StatusInternalServerError, "failed to write database", err)
		return
	}

	log.Printf("created new blog article '%s'", article.Title)

	WriteResponse(writer, http.StatusOK, map[string]interface{}{
		"id": id,
	})
}

func (h *ApiHandler) ServeBlogGetSingle(writer http.ResponseWriter, request *http.Request) {
	idStr := request.PathValue("id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		WriteError(writer, http.StatusBadRequest, "invalid id", err)
		return
	}

	article, err := h.db.GetBlogArticle(IsAuthorized(request), id)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			WriteError(writer, http.StatusNotFound, "article not found", nil)
		} else {
			WriteError(writer, http.StatusInternalServerError, "failed to query database", err)
		}
		return
	}

	WriteResponse(writer, http.StatusOK, article)
}

func (h *ApiHandler) ServeBlogPatch(writer http.ResponseWriter, request *http.Request) {
	idStr := request.PathValue("id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		WriteError(writer, http.StatusBadRequest, "invalid id", err)
		return
	}

	err = request.ParseMultipartForm(maxArticleFormSize)
	if err != nil {
		WriteError(writer, http.StatusBadRequest, "failed to parse multipart form", nil)
		return
	}

	file, _, err := request.FormFile("file")
	if err != nil {
		WriteError(writer, http.StatusBadRequest, "failed to parse file", nil)
		return
	}
	defer func() {
		_ = file.Close()
	}()

	article, err := ParseArticle(file, "/api/blog/"+strconv.FormatInt(id, 10)+"/file/")
	if err != nil {
		WriteError(writer, http.StatusInternalServerError, "failed to parse article: "+err.Error(), nil)
		return
	}

	article.Id = id
	err = h.db.UpdateBlogArticle(article)
	if err != nil {
		WriteError(writer, http.StatusInternalServerError, "failed to write database", err)
		return
	}

	log.Printf("updated blog article '%s' with id %d", article.Title, article.Id)

	WriteResponse(writer, http.StatusOK, map[string]interface{}{})
}

func (h *ApiHandler) ServeBlogDelete(writer http.ResponseWriter, request *http.Request) {
	idStr := request.PathValue("id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		WriteError(writer, http.StatusBadRequest, "invalid id", err)
		return
	}

	err = h.db.DeleteBlogArticle(id)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			WriteError(writer, http.StatusNotFound, "article not found", nil)
		} else {
			WriteError(writer, http.StatusInternalServerError, "failed to update database", err)
		}
		return
	}

	WriteResponse(writer, http.StatusOK, map[string]interface{}{})
}

func (h *ApiHandler) ServeBlogFileGetSingle(writer http.ResponseWriter, request *http.Request) {
	idStr := request.PathValue("articleId")
	articleId, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		WriteError(writer, http.StatusBadRequest, "invalid article id", err)
		return
	}

	idStr = request.PathValue("fileId")
	fileId, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		WriteError(writer, http.StatusBadRequest, "invalid file id", err)
		return
	}

	file, err := h.db.GetBlogArticleFile(IsAuthorized(request), articleId, fileId)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			WriteError(writer, http.StatusNotFound, "article file not found", nil)
		} else {
			WriteError(writer, http.StatusInternalServerError, "failed to query database", err)
		}
		return
	}

	_, err = writer.Write(file.Data)
	if err != nil {
		log.Println(err)
	}
}

func (h *ApiHandler) ServeBlogPostPublish(writer http.ResponseWriter, request *http.Request) {
	idStr := request.PathValue("id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		WriteError(writer, http.StatusBadRequest, "invalid id", err)
		return
	}

	err = h.db.SetBlogArticleStatus(id, ArticleStatusPublished)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			WriteError(writer, http.StatusNotFound, "article not found", nil)
		} else {
			WriteError(writer, http.StatusInternalServerError, "failed to update database", err)
		}
		return
	}

	WriteResponse(writer, http.StatusOK, map[string]interface{}{})
}

func (h *ApiHandler) ServeBlogPostUnpublish(writer http.ResponseWriter, request *http.Request) {
	idStr := request.PathValue("id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		WriteError(writer, http.StatusBadRequest, "invalid id", err)
		return
	}

	err = h.db.SetBlogArticleStatus(id, ArticleStatusOffline)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			WriteError(writer, http.StatusNotFound, "article not found", nil)
		} else {
			WriteError(writer, http.StatusInternalServerError, "failed to update database", err)
		}
		return
	}

	WriteResponse(writer, http.StatusOK, map[string]interface{}{})
}

func (h *ApiHandler) ServeBlogTagsGet(writer http.ResponseWriter, request *http.Request) {
	query := request.URL.Query()

	var err error
	var offset int
	var limit int

	offsetStr := query.Get("offset")
	if offsetStr == "" {
		offset = 0
	} else {
		offset, err = strconv.Atoi(offsetStr)
		if err != nil || offset < 0 {
			WriteError(writer, http.StatusBadRequest, "invalid offset", nil)
			return
		}
	}

	limitStr := query.Get("limit")
	if limitStr == "" {
		limit = 50
	} else {
		limit, err = strconv.Atoi(limitStr)
		if err != nil || limit <= 0 || limit > 100 {
			WriteError(writer, http.StatusBadRequest, "invalid limit", nil)
			return
		}
	}

	tags, total, err := h.db.GetBlogTags(IsAuthorized(request), offset, limit)
	if err != nil {
		WriteError(writer, http.StatusInternalServerError, "failed to query database", err)
		return
	}

	WriteResponse(writer, http.StatusOK, map[string]interface{}{
		"tags":  tags,
		"total": total,
	})
}
