package qrzws

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

	root "git.paulbsd.com/paulbsd/qrz"
	"git.paulbsd.com/paulbsd/qrz/src/config"
	"git.paulbsd.com/paulbsd/qrz/src/page"
	"git.paulbsd.com/paulbsd/qrz/src/qrz"
	"git.paulbsd.com/paulbsd/qrz/src/static"
	"git.paulbsd.com/paulbsd/qrz/src/templates"

	"github.com/labstack/echo/v4"
)

var tablename = strings.ToLower(strings.Split(fmt.Sprintf("%v", reflect.TypeOf(qrz.Qrz{})), ".")[1])

var colre = regexp.MustCompile(`^[A-Za-z0-9]+$`)
var orderre = regexp.MustCompile(`^(ASC|asc|DESC|desc)$`)
var intre = regexp.MustCompile(`^[0-9]+$`)

// RunServer runs the main echo server
func RunServer(config config.Config) (err error) {
	var p page.Page

	builtTemplates, err := templates.BuildTemplates(&root.Files)
	if err != nil {
		log.Println(err)
	}

	e := echo.New()
	e.HideBanner = true
	e.Renderer = builtTemplates

	e.GET("/", p.Index)
	e.GET("/health", func(c echo.Context) error {
		return c.String(http.StatusOK, "OK")
	})
	e.GET("/static/*", func(c echo.Context) error {
		return static.GetStatic(&root.Files, c)
	})
	e.POST("/qrzws", func(c echo.Context) (err error) {
		res, err := Run(c, config)
		if err != nil {
			return c.String(http.StatusInternalServerError,
				fmt.Sprintf("Error 500 %s", err))
		}
		return c.JSON(http.StatusOK, res)
	})
	e.GET("/export_frs.csv", func(c echo.Context) (err error) {
		data, err := RunCSVExport(c, config)
		if err != nil {
			return c.String(http.StatusInternalServerError,
				fmt.Sprintf("Error 500 %s", err))
		}
		return c.Blob(http.StatusOK, "text/csv", data)
	})

	if !config.NoFeed {
		go qrz.Run(config)
	}
	e.Logger.Fatal(e.Start(fmt.Sprintf("%s:%d", config.Host, config.Port)))
	return
}

// Run runs the main loop
func Run(c echo.Context, config config.Config) (res QrzDatatableOutput, err error) {
	qrzinputjson := new(QrzDatatableInput)

	if err = c.Bind(qrzinputjson); err != nil {
		return
	}

	rows, err := BuildQuery(config, qrzinputjson)
	if err != nil {
		return
	}

	if len(rows) > 0 {
		for _, b := range rows {
			res.Data = append(res.Data, qrz.ToSlice(b))
		}
	} else {
		res.Data = append(res.Data, []string{"", "", "", "", "", ""})
	}

	res.RecordsFiltered, err = BuildQCntFil(config, qrzinputjson)
	if err != nil {
		return
	}

	res.RecordsTotal, err = BuildQCntTot(config, qrzinputjson)
	if err != nil {
		return
	}

	res.Draw = qrzinputjson.Draw

	return
}

// BuildQuery builds main query
func BuildQuery(config config.Config, qrzdt *QrzDatatableInput) (qrz []qrz.Qrz, err error) {
	selectstatement, err := SetSelect(config, qrzdt)
	if err != nil {
		return nil, err
	}

	orderdir, ordercol, err := SetOrder(config, qrzdt)
	if err != nil {
		return nil, err
	}

	limit, offset, err := SetLimit(config, qrzdt)
	if err != nil {
		return nil, err
	}

	searchstatement, err := SetSearchLike(config, qrzdt)
	if err != nil {
		return nil, err
	}

	if orderdir == "desc" {
		err = config.Db.Where(searchstatement).Limit(
			limit, offset).Desc(
			ordercol...).Select(selectstatement).Find(&qrz)
	} else if orderdir == "asc" {
		err = config.Db.Where(searchstatement).Limit(
			limit, offset).Asc(
			ordercol...).Select(selectstatement).Find(&qrz)
	} else {
		err = config.Db.Where(searchstatement).Limit(
			limit, offset).Asc(
			"qrz").Select(selectstatement).Find(&qrz)
	}

	return
}

// BuildQCntFil builds query for counting filtered
func BuildQCntFil(config config.Config, qrzdt *QrzDatatableInput) (cnt int64, err error) {
	searchstatement, err := SetSearchLike(config, qrzdt)
	if err != nil {
		log.Println(err)
	}
	cnt, err = config.Db.SQL(
		fmt.Sprintf("SELECT COUNT(*) FROM %s WHERE %s", tablename,
			searchstatement)).Count()

	return
}

// BuildQCntTot builds query for counting totals
func BuildQCntTot(config config.Config, qrzdt *QrzDatatableInput) (cnt int64, err error) {
	var qrz qrz.Qrz
	cnt, err = config.Db.Count(&qrz)

	return
}

// SetSelect build the sql select statement part
func SetSelect(config config.Config, qrzdt *QrzDatatableInput) (selectstatement string, err error) {
	var cols []string

	if len(qrzdt.Columns) > 0 {
		for _, col := range qrzdt.Columns {
			valid := colre.MatchString(col.Name)
			if valid {
				cols = append(cols, col.Name)
			} else {
				err = fmt.Errorf("string does not match prerequisites")
				return
			}
		}
		selectstatement = strings.Join(cols, ",")
	} else {
		selectstatement = "*"
	}

	return
}

// SetOrder build the sql order statement part
func SetOrder(config config.Config, qrzdt *QrzDatatableInput) (orderdir string, ordercols []string, err error) {
	for _, col := range qrzdt.Order {
		if colre.MatchString(qrzdt.Columns[col.Column].Name) &&
			orderre.MatchString(col.Dir) {
			ordercols = append(ordercols, qrzdt.Columns[col.Column].Name)
			orderdir = col.Dir
		} else {
			err = fmt.Errorf("order statements does not match prerequisites")
			return
		}
	}

	return
}

// SetLimit build the sql limit
func SetLimit(config config.Config, qrzdt *QrzDatatableInput) (limit int, offset int, err error) {

	if qrzdt.Length < 1 {
		qrzdt.Length = 50
	}

	if intre.MatchString(
		fmt.Sprintf("%d", qrzdt.Length)) && intre.MatchString(
		fmt.Sprintf("%d", qrzdt.Start)) {
		limit = qrzdt.Length
		offset = qrzdt.Start
	} else {
		err = fmt.Errorf("limit statements does not match prerequisites")
		return
	}

	return
}

// SetSearchLike build the where clause in sql statement
func SetSearchLike(config config.Config, qrzdt *QrzDatatableInput) (searchstmt string, err error) {
	var searchstmtslice []string
	if len(qrzdt.Columns) > 0 {
		var searchstr string
		switch config.DbType {
		case "sqlite3":
			searchstr = "%s LIKE '%s%%'"
		case "mysql":
			searchstr = "%s LIKE '%s%%'"
		case "postgresql":
			searchstr = "%s ~* '%s'"
		default:
			searchstr = "%s ~* '%s'"
		}
		for id, i := range qrzdt.Columns {
			searchstmtslice = append(searchstmtslice,
				fmt.Sprintf(searchstr, i.Name, qrzdt.Search.Value))
			if id < len(qrzdt.Columns)-1 {
				searchstmtslice = append(searchstmtslice, "OR")
			}
		}

	} else {
		searchstmtslice = []string{"1=1"}
	}

	searchstmt = strings.Join(searchstmtslice, " ")

	return
}

// RunCSVExport runs the main loop
func RunCSVExport(c echo.Context, config config.Config) (data []byte, err error) {
	var q []qrz.Qrz
	var res = []string{"QRZ,Name,City,Dept,Country,DMRID"}
	err = config.Db.Asc("qrz").Find(&q)
	for _, item := range q {
		res = append(res, strings.Join(qrz.ToSlice(item), ","))
	}
	data = []byte(strings.Join(res, "\n"))

	return
}

// QrzDatatableInput describes datatable query
type QrzDatatableInput struct {
	Columns []struct {
		Data       int    `json:"data"`
		Name       string `json:"name"`
		Searchable bool   `json:"searchable"`
		Orderable  bool   `json:"orderable"`
		Search     struct {
			Regex bool   `json:"regex"`
			Value string `json:"value"`
		} `json:"search"`
	} `json:"columns"`
	Draw   int `json:"draw"`
	Length int `json:"length"`
	Order  []struct {
		Column int    `json:"column"`
		Dir    string `json:"dir"`
	} `json:"order"`
	Search struct {
		Regex bool   `json:"regex"`
		Value string `json:"value"`
	} `json:"search"`
	Start int `json:"start"`
}

// QrzDatatableOutput describes datatable response
type QrzDatatableOutput struct {
	Data            [][]string `json:"data"`
	Draw            int        `json:"draw"`
	RecordsTotal    int64      `json:"recordsTotal"`
	RecordsFiltered int64      `json:"recordsFiltered"`
}
