/*
*
*	Gosora Topic Store
*	Copyright Azareal 2017 - 2020
*
 */
package common

import (
	"database/sql"
	"errors"
	"strconv"
	"strings"

	"github.com/Azareal/Gosora/query_gen"
)

// TODO: Add the watchdog goroutine
// TODO: Add some sort of update method
// ? - Should we add stick, lock, unstick, and unlock methods? These might be better on the Topics not the TopicStore
var Topics TopicStore
var ErrNoTitle = errors.New("This message is missing a title")
var ErrLongTitle = errors.New("The title is too long")
var ErrNoBody = errors.New("This message is missing a body")

type TopicStore interface {
	DirtyGet(id int) *Topic
	Get(id int) (*Topic, error)
	BypassGet(id int) (*Topic, error)
	BulkGetMap(ids []int) (list map[int]*Topic, err error)
	Exists(id int) bool
	Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error)
	AddLastTopic(item *Topic, fid int) error // unimplemented
	Reload(id int) error                     // Too much SQL logic to move into TopicCache
	// TODO: Implement these two methods
	//Replies(tid int) ([]*Reply, error)
	//RepliesRange(tid int, lower int, higher int) ([]*Reply, error)
	Count() int

	SetCache(cache TopicCache)
	GetCache() TopicCache
}

type DefaultTopicStore struct {
	cache TopicCache

	get        *sql.Stmt
	exists     *sql.Stmt
	count *sql.Stmt
	create     *sql.Stmt
}

// NewDefaultTopicStore gives you a new instance of DefaultTopicStore
func NewDefaultTopicStore(cache TopicCache) (*DefaultTopicStore, error) {
	acc := qgen.NewAcc()
	if cache == nil {
		cache = NewNullTopicCache()
	}
	return &DefaultTopicStore{
		cache:      cache,
		get:        acc.Select("topics").Columns("title, content, createdBy, createdAt, lastReplyBy, lastReplyAt, lastReplyID, is_closed, sticky, parentID, ipaddress, views, postCount, likeCount, attachCount, poll, data").Where("tid = ?").Prepare(),
		exists:     acc.Select("topics").Columns("tid").Where("tid = ?").Prepare(),
		count: acc.Count("topics").Prepare(),
		create:     acc.Insert("topics").Columns("parentID, title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, ipaddress, words, createdBy").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?").Prepare(),
	}, acc.FirstError()
}

func (s *DefaultTopicStore) DirtyGet(id int) *Topic {
	topic, err := s.cache.Get(id)
	if err == nil {
		return topic
	}
	topic, err = s.BypassGet(id)
	if err == nil {
		_ = s.cache.Set(topic)
		return topic
	}
	return BlankTopic()
}

// TODO: Log weird cache errors?
func (s *DefaultTopicStore) Get(id int) (topic *Topic, err error) {
	topic, err = s.cache.Get(id)
	if err == nil {
		return topic, nil
	}
	topic, err = s.BypassGet(id)
	if err == nil {
		_ = s.cache.Set(topic)
	}
	return topic, err
}

// BypassGet will always bypass the cache and pull the topic directly from the database
func (s *DefaultTopicStore) BypassGet(id int) (*Topic, error) {
	t := &Topic{ID: id}
	err := s.get.QueryRow(id).Scan(&t.Title, &t.Content, &t.CreatedBy, &t.CreatedAt, &t.LastReplyBy, &t.LastReplyAt, &t.LastReplyID, &t.IsClosed, &t.Sticky, &t.ParentID, &t.IP, &t.ViewCount, &t.PostCount, &t.LikeCount, &t.AttachCount, &t.Poll, &t.Data)
	if err == nil {
		t.Link = BuildTopicURL(NameToSlug(t.Title), id)
	}
	return t, err
}

// TODO: Avoid duplicating much of this logic from user_store.go
func (s *DefaultTopicStore) BulkGetMap(ids []int) (list map[int]*Topic, err error) {
	idCount := len(ids)
	list = make(map[int]*Topic)
	if idCount == 0 {
		return list, nil
	}

	var stillHere []int
	sliceList := s.cache.BulkGet(ids)
	if len(sliceList) > 0 {
		for i, sliceItem := range sliceList {
			if sliceItem != nil {
				list[sliceItem.ID] = sliceItem
			} else {
				stillHere = append(stillHere, ids[i])
			}
		}
		ids = stillHere
	}

	// If every user is in the cache, then return immediately
	if len(ids) == 0 {
		return list, nil
	} else if len(ids) == 1 {
		topic, err := s.Get(ids[0])
		if err != nil {
			return list, err
		}
		list[topic.ID] = topic
		return list, nil
	}

	// TODO: Add a function for the qlist stuff
	var q string
	idList := make([]interface{},len(ids))
	for i, id := range ids {
		idList[i] = strconv.Itoa(id)
		q += "?,"
	}
	q = q[0 : len(q)-1]

	rows, err := qgen.NewAcc().Select("topics").Columns("tid,title,content,createdBy,createdAt,lastReplyBy,lastReplyAt,lastReplyID,is_closed,sticky,parentID,ipaddress,views,postCount,likeCount,attachCount,poll,data").Where("tid IN(" + q + ")").Query(idList...)
	if err != nil {
		return list, err
	}
	defer rows.Close()

	for rows.Next() {
		t := &Topic{}
		err := rows.Scan(&t.ID, &t.Title, &t.Content, &t.CreatedBy, &t.CreatedAt, &t.LastReplyBy, &t.LastReplyAt, &t.LastReplyID, &t.IsClosed, &t.Sticky, &t.ParentID, &t.IP, &t.ViewCount, &t.PostCount, &t.LikeCount, &t.AttachCount, &t.Poll, &t.Data)
		if err != nil {
			return list, err
		}
		t.Link = BuildTopicURL(NameToSlug(t.Title), t.ID)
		s.cache.Set(t)
		list[t.ID] = t
	}
	err = rows.Err()
	if err != nil {
		return list, err
	}

	// Did we miss any topics?
	if idCount > len(list) {
		var sidList string
		for _, id := range ids {
			_, ok := list[id]
			if !ok {
				sidList += strconv.Itoa(id) + ","
			}
		}
		if sidList != "" {
			sidList = sidList[0 : len(sidList)-1]
			err = errors.New("Unable to find topics with the following IDs: " + sidList)
		}
	}

	return list, err
}

func (s *DefaultTopicStore) Reload(id int) error {
	topic, err := s.BypassGet(id)
	if err == nil {
		_ = s.cache.Set(topic)
	} else {
		_ = s.cache.Remove(id)
	}
	TopicListThaw.Thaw()
	return err
}

func (s *DefaultTopicStore) Exists(id int) bool {
	return s.exists.QueryRow(id).Scan(&id) == nil
}

func (s *DefaultTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) {
	if topicName == "" {
		return 0, ErrNoTitle
	}
	// ? This number might be a little screwy with Unicode, but it's the only consistent thing we have, as Unicode characters can be any number of bytes in theory?
	if len(topicName) > Config.MaxTopicTitleLength {
		return 0, ErrLongTitle
	}

	parsedContent := strings.TrimSpace(ParseMessage(content, fid, "forums"))
	if parsedContent == "" {
		return 0, ErrNoBody
	}

	wcount := WordCount(content)
	// TODO: Move this statement into the topic store
	res, err := s.create.Exec(fid, topicName, content, parsedContent, uid, ipaddress, wcount, uid)
	if err != nil {
		return 0, err
	}

	lastID, err := res.LastInsertId()
	if err != nil {
		return 0, err
	}

	return int(lastID), Forums.AddTopic(int(lastID), uid, fid)
}

// ? - What is this? Do we need it? Should it be in the main store interface?
func (s *DefaultTopicStore) AddLastTopic(item *Topic, fid int) error {
	// Coming Soon...
	return nil
}

// Count returns the total number of topics on these forums
func (s *DefaultTopicStore) Count() (count int) {
	err := s.count.QueryRow().Scan(&count)
	if err != nil {
		LogError(err)
	}
	return count
}

func (s *DefaultTopicStore) SetCache(cache TopicCache) {
	s.cache = cache
}

// TODO: We're temporarily doing this so that you can do tcache != nil in getTopicUser. Refactor it.
func (s *DefaultTopicStore) GetCache() TopicCache {
	_, ok := s.cache.(*NullTopicCache)
	if ok {
		return nil
	}
	return s.cache
}