/* Copyright (C) 2019 Monomax Software Pty Ltd
 *
 * This file is part of Dnote.
 *
 * Dnote is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Dnote is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Dnote.  If not, see <https://www.gnu.org/licenses/>.
 */

package handlers

import (
	"encoding/json"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/dnote/dnote/pkg/server/api/helpers"
	"github.com/dnote/dnote/pkg/server/api/presenters"
	"github.com/dnote/dnote/pkg/server/database"
	"github.com/gorilla/mux"
	"github.com/jinzhu/gorm"
	"github.com/pkg/errors"
)

func respondWithNote(w http.ResponseWriter, note database.Note) {
	presentedNote := presenters.PresentNote(note)

	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(presentedNote); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func preloadNote(conn *gorm.DB) *gorm.DB {
	return conn.Preload("Book").Preload("User")
}

func (a *App) getDemoNote(w http.ResponseWriter, r *http.Request) {
	db := database.DBConn
	vars := mux.Vars(r)
	noteUUID := vars["noteUUID"]

	demoUserID, err := helpers.GetDemoUserID()
	if err != nil {
		http.Error(w, errors.Wrap(err, "finding demo user").Error(), http.StatusInternalServerError)
		return
	}

	var note database.Note
	conn := db.Where("uuid = ? AND user_id = ?", noteUUID, demoUserID)
	conn = preloadNote(conn)
	conn.Find(&note)

	if conn.RecordNotFound() {
		http.Error(w, "not found", http.StatusNotFound)
		return
	} else if err := conn.Error; err != nil {
		http.Error(w, errors.Wrap(err, "finding note").Error(), http.StatusInternalServerError)
		return
	}

	respondWithNote(w, note)
}

func (a *App) getNote(w http.ResponseWriter, r *http.Request) {
	user, ok := r.Context().Value(helpers.KeyUser).(database.User)
	if !ok {
		http.Error(w, "No authenticated user found", http.StatusInternalServerError)
		return
	}

	db := database.DBConn
	vars := mux.Vars(r)
	noteUUID := vars["noteUUID"]

	var note database.Note
	conn := db.Debug().Where("uuid = ? AND user_id = ?", noteUUID, user.ID)
	conn = preloadNote(conn)
	conn.Find(&note)

	if conn.RecordNotFound() {
		http.Error(w, "not found", http.StatusNotFound)
		return
	} else if err := conn.Error; err != nil {
		http.Error(w, errors.Wrap(err, "finding note").Error(), http.StatusInternalServerError)
		return
	}

	respondWithNote(w, note)
}

/**** getNotesHandler */

// GetNotesResponse is a reponse by getNotesHandler
type GetNotesResponse struct {
	Notes    []presenters.Note `json:"notes"`
	Total    int               `json:"total"`
	PrevDate *int64            `json:"prev_date"`
}

func (a *App) getDemoNotes(w http.ResponseWriter, r *http.Request) {
	userID, err := helpers.GetDemoUserID()
	if err != nil {
		http.Error(w, errors.Wrap(err, "finding demo user id").Error(), http.StatusInternalServerError)
		return
	}
	query := r.URL.Query()

	respondGetNotes(userID, query, w)
}

func (a *App) getNotes(w http.ResponseWriter, r *http.Request) {
	user, ok := r.Context().Value(helpers.KeyUser).(database.User)
	if !ok {
		http.Error(w, "No authenticated user found", http.StatusInternalServerError)
		return
	}
	query := r.URL.Query()

	respondGetNotes(user.ID, query, w)
}

func respondGetNotes(userID int, query url.Values, w http.ResponseWriter) {
	err := validateGetNotesQuery(query)
	if err != nil {
		http.Error(w, errors.Wrap(err, "validating query parameters").Error(), http.StatusBadRequest)
		return
	}

	q, err := parseGetNotesQuery(query)
	if err != nil {
		http.Error(w, errors.Wrap(err, "parsing query parameters").Error(), http.StatusBadRequest)
		return
	}

	dateLowerbound, dateUpperbound := getDateBounds(q.Year, q.Month)

	baseConn := getNotesBaseQuery(userID, q)
	conn := baseConn.Where("notes.added_on >= ? AND notes.added_on < ?", dateLowerbound, dateUpperbound)

	var total int
	err = conn.Debug().Model(database.Note{}).Count(&total).Error
	if err != nil {
		http.Error(w, errors.Wrap(err, "counting total").Error(), http.StatusInternalServerError)
		return
	}

	notes := []database.Note{}
	if total != 0 {
		conn = orderGetNotes(conn)
		conn = preloadNote(conn)
		conn = paginate(conn, q.Page)

		err = conn.Find(&notes).Error
		if err != nil {
			http.Error(w, errors.Wrap(err, "finding notes").Error(), http.StatusInternalServerError)
			return
		}
	}

	// peek the prev date
	var prevDateUpperbound int64
	if len(notes) > 0 {
		lastNote := notes[len(notes)-1]
		prevDateUpperbound = lastNote.AddedOn
	} else {
		prevDateUpperbound = dateLowerbound
	}

	prevDate, err := getPrevDate(baseConn, prevDateUpperbound)
	if err != nil {
		http.Error(w, errors.Wrap(err, "getting prevDate").Error(), http.StatusInternalServerError)
		return
	}

	presentedNotes := presenters.PresentNotes(notes)

	response := GetNotesResponse{
		Notes:    presentedNotes,
		Total:    total,
		PrevDate: prevDate,
	}

	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(response); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func getPrevDate(baseConn *gorm.DB, dateUpperbound int64) (*int64, error) {
	var prevNote database.Note

	conn := baseConn.
		Select("notes.added_on").
		Where("notes.added_on < ?", dateUpperbound).
		Order("notes.added_on DESC")

	if conn.First(&prevNote).RecordNotFound() {
		return nil, nil
	}

	if err := conn.Error; err != nil {
		return nil, errors.Wrap(err, "querying previous note")
	}

	return &prevNote.AddedOn, nil
}

func validateGetNotesQuery(q url.Values) error {
	if q.Get("year") == "" {
		return errors.New("'year' is required")
	}
	if q.Get("month") == "" {
		return errors.New("'month' is required")
	}

	return nil
}

type getNotesQuery struct {
	Year      int
	Month     int
	Page      int
	BookUUID  string
	Encrypted *bool
}

func parseGetNotesQuery(q url.Values) (getNotesQuery, error) {
	yearStr := q.Get("year")
	monthStr := q.Get("month")
	bookStr := q.Get("book")
	pageStr := q.Get("page")
	encryptedStr := q.Get("encrypted")

	var page int
	if len(pageStr) > 0 {
		p, err := strconv.Atoi(pageStr)
		if err != nil {
			return getNotesQuery{}, errors.Wrap(err, "parsing page")
		}

		page = p
	} else {
		page = 1
	}

	year, err := strconv.Atoi(yearStr)
	if err != nil {
		return getNotesQuery{}, errors.Wrapf(err, "invalid year %s", yearStr)
	}
	month, err := strconv.Atoi(monthStr)
	if err != nil {
		return getNotesQuery{}, errors.Wrapf(err, "invalid month %s", monthStr)
	}
	if month < 1 || month > 12 {
		return getNotesQuery{}, errors.Errorf("Invalid month %s", monthStr)
	}

	var encrypted *bool
	if strings.ToLower(encryptedStr) == "true" {
		*encrypted = true
	} else if strings.ToLower(encryptedStr) == "false" {
		*encrypted = false
	}

	ret := getNotesQuery{
		Year:      year,
		Month:     month,
		Page:      page,
		BookUUID:  bookStr,
		Encrypted: encrypted,
	}

	return ret, nil
}

func getDateBounds(year, month int) (int64, int64) {
	var yearUpperbound, monthUpperbound int

	if month == 12 {
		monthUpperbound = 1
		yearUpperbound = year + 1
	} else {
		monthUpperbound = month + 1
		yearUpperbound = year
	}

	lower := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC).UnixNano()
	upper := time.Date(yearUpperbound, time.Month(monthUpperbound), 1, 0, 0, 0, 0, time.UTC).UnixNano()

	return lower, upper
}

func getNotesBaseQuery(userID int, q getNotesQuery) *gorm.DB {
	db := database.DBConn

	conn := db.Where("notes.user_id = ? AND notes.deleted = ?", userID, false)

	if len(q.BookUUID) > 0 {
		conn = conn.Joins("INNER JOIN books ON books.uuid = notes.book_uuid").
			Where("books.uuid = ?", q.BookUUID)
	}
	if q.Encrypted != nil {
		conn = conn.Where("notes.encrypted = ?", *q.Encrypted)
	}

	return conn
}

func orderGetNotes(conn *gorm.DB) *gorm.DB {
	return conn.Order("notes.added_on DESC, notes.id DESC")
}

func (a *App) legacyGetNotes(w http.ResponseWriter, r *http.Request) {
	user, ok := r.Context().Value(helpers.KeyUser).(database.User)
	if !ok {
		http.Error(w, "No authenticated user found", http.StatusInternalServerError)
		return
	}

	var notes []database.Note
	db := database.DBConn
	if err := db.Where("user_id = ? AND encrypted = false", user.ID).Find(&notes).Error; err != nil {
		http.Error(w, "finding notes", http.StatusInternalServerError)
		return
	}

	presented := presenters.PresentNotes(notes)
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(presented); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}
