// package core is the collection of re-usable functions that primarily provides data (DB / CRUD) operations
// to the app. For instance, creating and mutating objects like lists, subscribers etc.
// All such methods return an echo.HTTPError{} (which implements error.error) that can be directly returned
// as a response to HTTP handlers without further processing.
package core

import (
	"bytes"
	"fmt"
	"log"
	"net/http"
	"regexp"
	"strings"

	"github.com/jmoiron/sqlx"
	"source.offmarket.win/aleagle/eaglecast/internal/i18n"
	"source.offmarket.win/aleagle/eaglecast/models"
	"github.com/labstack/echo/v4"
	"github.com/lib/pq"
)

const (
	SortAsc  = "asc"
	SortDesc = "desc"

	matDashboardCharts = "mat_dashboard_charts"
	matDashboardCounts = "mat_dashboard_counts"
	matListSubStats    = "mat_list_subscriber_stats"
)

// Core represents the eaglecast core with all shared, global functions.
type Core struct {
	h *Hooks

	consts Constants
	i18n   *i18n.I18n
	db     *sqlx.DB
	q      *models.Queries
	log    *log.Logger
}

// Constants represents constant config.
type Constants struct {
	SendOptinConfirmation bool
	BounceActions         map[string]struct {
		Count  int
		Action string
	}
	CacheSlowQueries bool
}

// Hooks contains external function hooks that are required by the core package.
type Hooks struct {
	SendOptinConfirmation func(models.Subscriber, []int) (int, error)
}

// Opt contains the controllers required to start the core.
type Opt struct {
	Constants Constants
	I18n      *i18n.I18n
	DB        *sqlx.DB
	Queries   *models.Queries
	Log       *log.Logger
}

var (
	ErrNotFound = echo.NewHTTPError(http.StatusNotFound, "not found")
)

var (
	regexFullTextQuery  = regexp.MustCompile(`\s+`)
	regexpSpaces        = regexp.MustCompile(`[\s]+`)
	campQuerySortFields = []string{"name", "status", "created_at", "updated_at"}
	subQuerySortFields  = []string{"email", "status", "name", "created_at", "updated_at"}
	listQuerySortFields = []string{"name", "status", "created_at", "updated_at", "subscriber_count"}
)

// New returns a new instance of the core.
func New(o *Opt, h *Hooks) *Core {
	return &Core{
		h:      h,
		consts: o.Constants,
		i18n:   o.I18n,
		db:     o.DB,
		q:      o.Queries,
		log:    o.Log,
	}
}

// RefreshMatViews refreshes all materialized views.
func (c *Core) RefreshMatViews(concurrent bool) error {
	for _, v := range []string{matDashboardCharts, matDashboardCounts, matListSubStats} {
		_ = c.RefreshMatView(v, true)
	}
	return nil
}

// RefreshMatView refreshes a Postgres materialized view.
func (c *Core) RefreshMatView(name string, concurrent bool) error {
	q := "REFRESH MATERIALIZED VIEW %s %s"
	if concurrent {
		q = fmt.Sprintf(q, "CONCURRENTLY", name)
	} else {
		q = fmt.Sprintf(q, "", name)
	}

	if _, err := c.db.Exec(q); err != nil {
		c.log.Printf("error refreshing materialized view: %s: %v", name, err)
		return err
	}

	return nil
}

// refreshCache refreshes a Postgres materialized view if caching is disabled.
func (c *Core) refreshCache(name string, concurrent bool) error {
	if c.consts.CacheSlowQueries {
		return nil
	}

	return c.RefreshMatView(name, concurrent)
}

// Given an error, pqErrMsg will try to return pq error details
// if it's a pq error.
func pqErrMsg(err error) string {
	if err, ok := err.(*pq.Error); ok {
		if err.Detail != "" {
			return fmt.Sprintf("%s. %s", err, err.Detail)
		}
	}
	return err.Error()
}

// makeSearchQuery cleans an optional search string and prepares the
// query SQL statement (string interpolated) and returns the
// search query string along with the SQL expression.
func makeSearchQuery(searchStr, orderBy, order, query string, querySortFields []string) (string, string) {
	searchStr = makeSearchString(searchStr)

	// Sort params.
	if !strSliceContains(orderBy, querySortFields) {
		orderBy = "created_at"
	}
	if order != SortAsc && order != SortDesc {
		order = SortDesc
	}

	query = strings.ReplaceAll(query, "%order%", orderBy+" "+order)

	return searchStr, query
}

// makeSearchString prepares a search string for use in both tsquery and ILIKE queries.
func makeSearchString(searchStr string) string {
	if searchStr == "" {
		return ""
	}
	return `%` + string(regexFullTextQuery.ReplaceAll([]byte(searchStr), []byte("&"))) + `%`
}

// strSliceContains checks if a string is present in the string slice.
func strSliceContains(str string, sl []string) bool {
	for _, s := range sl {
		if s == str {
			return true
		}
	}

	return false
}

// normalizeTags takes a list of string tags and normalizes them by
// lower casing and removing all special characters except for dashes.
func normalizeTags(tags []string) []string {
	var (
		out  []string
		dash = []byte("-")
	)

	for _, t := range tags {
		rep := regexpSpaces.ReplaceAll(bytes.TrimSpace([]byte(t)), dash)

		if len(rep) > 0 {
			out = append(out, string(rep))
		}
	}
	return out
}

// sanitizeSQLExp does basic sanitisation on arbitrary
// SQL query expressions coming from the frontend.
func sanitizeSQLExp(q string) string {
	if len(q) == 0 {
		return ""
	}
	q = strings.TrimSpace(q)

	// Remove semicolon suffix.
	if q[len(q)-1] == ';' {
		q = q[:len(q)-1]
	}
	return q
}

// strHasLen checks if the given string has a length within min-max.
func strHasLen(str string, min, max int) bool {
	return len(str) >= min && len(str) <= max
}
