package qrzws

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

	"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/gobuffalo/packr/v2"
	"github.com/jmoiron/sqlx"
	"github.com/labstack/echo/v4"
)

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

	templatesbox := packr.New("templates", "../../templates")
	staticbox := packr.New("static", "../../static")

	builtTemplates, _ := templates.BuildTemplates(templatesbox)

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

	e.GET("/", p.Index)
	e.GET("/static/*", func(c echo.Context) error {
		return static.GetStatic(staticbox, c)
	})
	e.POST("/qrzws", func(c echo.Context) (err error) {
		res, err := Run(c, config)
		if err != nil {
			return c.String(http.StatusInternalServerError, "Erreur 500 ta mère")
		}
		return c.JSON(http.StatusOK, res)
	})
	e.GET("/export_frs.csv", func(c echo.Context) (err error) {
		data, mime, err := RunCSVExport(c, config)
		if err != nil {
			log.Fatalln(err)
		}
		return c.Blob(http.StatusOK, mime, data)
	})

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

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

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

	rows, err := BuildQuery(config, *qrzinputjson)
	if err != nil {
		fmt.Println(err)
		return
	}
	for rows.Next() {
		var line []string
		results := make(map[string]interface{})
		err = rows.MapScan(results)
		colslice, _ := rows.Columns()
		for _, column := range colslice {
			if results[column] == nil {
				line = append(line, "")
			} else {
				line = append(line, fmt.Sprintf("%s", results[column]))
			}
		}
		res.Data = append(res.Data, line)
		count++
	}
	rows.Close()

	res.RecordsFiltered, err = BuildQueryCountFiltered(config, *qrzinputjson)
	res.RecordsTotal, err = BuildQueryCountTotal(config, *qrzinputjson)
	res.Draw = qrzinputjson.Draw

	return
}

// BuildQuery builds main query
func BuildQuery(config config.Config, qrzdt QrzDatatableInput) (rows *sqlx.Rows, err error) {
	var selectstatement, orderstatement, limitstatement, searchstatement string
	selectstatement, err = SetSelectStatement(config, qrzdt)
	if err != nil {
		return nil, err
	}
	orderstatement, err = SetOrderStatement(config, qrzdt)
	if err != nil {
		return nil, err
	}
	limitstatement, err = SetLimitStatement(config, qrzdt)
	if err != nil {
		return nil, err
	}
	searchstatement, err = SetSearchLikeStatement(config, qrzdt)
	if err != nil {
		return nil, err
	}

	rows, err = config.Db.Queryx(fmt.Sprintf("SELECT %s FROM %s WHERE %s %s %s;", selectstatement, config.DbTable, searchstatement, orderstatement, limitstatement))

	return
}

// BuildQueryCountFiltered builds query for counting filtered
func BuildQueryCountFiltered(config config.Config, qrzdt QrzDatatableInput) (cnt int, err error) {
	searchstatement, err := SetSearchLikeStatement(config, qrzdt)

	query := `
		SELECT COUNT(*)
		FROM %s
		WHERE %s;`

	err = config.Db.Get(&cnt, fmt.Sprintf(query, config.DbTable, searchstatement))

	return
}

// BuildQueryCountTotal builds query for counting totals
func BuildQueryCountTotal(config config.Config, qrzdt QrzDatatableInput) (cnt int, err error) {
	query := `
		SELECT COUNT(*) FROM %s;
	`
	err = config.Db.Get(&cnt, fmt.Sprintf(query, config.DbTable))

	return
}

// SetSelectStatement build the sql select statement part
func SetSelectStatement(config config.Config, qrzdt QrzDatatableInput) (selectstatement string, err error) {
	var cols []string
	colre := regexp.MustCompile(`^[A-Za-z0-9]+$`)
	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
}

// SetOrderStatement build the sql order statement part
func SetOrderStatement(config config.Config, qrzdt QrzDatatableInput) (orderstmt string, err error) {
	var orderstmts []string
	colre := regexp.MustCompile(`^[A-Za-z0-9]+$`)
	orderre := regexp.MustCompile(`^(ASC|asc|DESC|desc)$`)
	for _, col := range qrzdt.Order {
		if colre.MatchString(qrzdt.Columns[col.Column].Name) && orderre.MatchString(col.Dir) {
			orderstmts = append(orderstmts, fmt.Sprintf("%s %s", qrzdt.Columns[col.Column].Name, col.Dir))
		} else {
			err = fmt.Errorf("Order statements does not match prerequisites")
			return
		}
	}
	if len(orderstmts) > 0 {
		orderstmt = fmt.Sprintf("ORDER BY %s", strings.Join(orderstmts, ", "))
	}
	return
}

// SetLimitStatement build the sql limit statement part
func SetLimitStatement(config config.Config, qrzdt QrzDatatableInput) (limitstmt string, err error) {
	intre := regexp.MustCompile(`^[0-9]+$`)
	if qrzdt.Length < 1 {
		qrzdt.Length = 50
	}
	if intre.MatchString(fmt.Sprintf("%d", qrzdt.Length)) && intre.MatchString(fmt.Sprintf("%d", qrzdt.Start)) {
		limitstmt = fmt.Sprintf("LIMIT %d OFFSET %d", qrzdt.Length, qrzdt.Start)
	} else {
		err = fmt.Errorf("Limit statements does not match prerequisites")
		return
	}
	return
}

// SetSearchLikeStatement build the where clause in sql statement
func SetSearchLikeStatement(config config.Config, qrzdt QrzDatatableInput) (searchstmt string, err error) {
	var searchstmtslice []string
	if len(qrzdt.Columns) > 0 {
		for id, i := range qrzdt.Columns {
			searchstmtslice = append(searchstmtslice, fmt.Sprintf("%s LIKE '%%%s%%'", 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
}

// SetSearchFTSStatement build the where clause in sql statement
func SetSearchFTSStatement(config config.Config, qrzdt QrzDatatableInput) (searchstmt string, err error) {
	if len(qrzdt.Columns) > 0 {
		searchstmt = fmt.Sprintf("fts @@ plainto_tsquery('%s')", qrzdt.Search.Value)
	} else {
		searchstmt = "1=1"
	}
	return
}

// RunCSVExport runs the main loop
func RunCSVExport(c echo.Context, config config.Config) (data []byte, mime string, err error) {
	mime = "text/csv"
	rows, err := config.Db.Queryx(config.DbStatements.ExportCSV)
	var res []string
	res = append(res, "QRZ,Name,City,Dept,Country")
	for rows.Next() {
		var l []string
		results := make(map[string]interface{})
		err = rows.MapScan(results)
		colslice, _ := rows.Columns()
		for _, column := range colslice {
			if results[column] == nil {
				l = append(l, "")
			} else {
				l = append(l, fmt.Sprintf("%s", results[column]))
			}
		}
		line := strings.Join(l, ",")
		res = append(res, line)
	}
	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    int        `json:"recordsTotal"`
	RecordsFiltered int        `json:"recordsFiltered"`
}
