package qrz

import (
	"database/sql"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"regexp"
	"strings"
	"time"

	"git.paulbsd.com/paulbsd/qrz/src/config"
	"github.com/antchfx/htmlquery"
	"github.com/jinzhu/gorm"
	"github.com/robfig/cron"
)

// InitCronConfig create task schedules
func InitCronConfig(config config.Config) (err error) {
	cr := cron.New()
	cr.AddFunc(config.Cron, func() { err = Run(config) })
	cr.Start()
	return
}

// Run runs the main loop
func Run(config config.Config) (err error) {
	for _, group := range config.QrzGroups {
		url := fmt.Sprintf(config.URLBase, group)
		fmt.Println(fmt.Sprintf("Processing the %s group", group))

		bodyStr, err := getBody(url)
		if err != nil {
			log.Fatalln(err)
		}

		frsPeople, err := getFrsEntries(config, bodyStr)
		if err != nil {
			log.Fatalln(err)
		}

		existingQrz, err := getCurrentEntries(config)
		if err != nil {
			log.Fatalln(err)
		}

		err = discardExistingEntries(config, &frsPeople, existingQrz)
		if err != nil {
			log.Fatalln(err)
		}

		if len(frsPeople) > 0 {
			err = insertFrsEntryToDB(config, frsPeople)
			if err != nil {
				log.Fatalln(err)
			}
		} else {
			fmt.Println(fmt.Sprintf("No entry to insert in database for group %s", group))
		}
	}
	return
}

// getBody fetch html page
func getBody(url string) (string, error) {
	var client http.Client

	resp, err := client.Get(url)
	if err != nil {
		return "", err
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	/*
		// For old website support 23/05/2020
		dec := charmap.ISO8859_1.NewDecoder()
		output, _ := dec.Bytes(body)
		bodyString := string(output)
	*/
	return string(body), nil
}

// getFrsEntries get FRS entries from html body
func getFrsEntries(config config.Config, body string) (frsentries map[string]FrsEntry, err error) {
	frsentries = make(map[string]FrsEntry)
	re := regexp.MustCompile(`^   [0-9]{1,4}\s[A-Z]{1,4}\s[0-9]{1,4}`)

	htmlpage, err := htmlquery.Parse(strings.NewReader(body))
	if err != nil {
		return
	}

	for _, n := range htmlquery.Find(htmlpage, "//tr") {
		td := htmlquery.Find(n, "//td")
		if len(td) > 2 {
			if re.MatchString(htmlquery.InnerText(td[1])) {
				frs := FrsEntry{
					QRZ:     strings.TrimLeft(htmlquery.InnerText(td[1]), " "),
					DMRID:   sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[0]), " "), Valid: true},
					Name:    sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[2]), " "), Valid: true},
					City:    sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[3]), " "), Valid: true},
					Dept:    sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[4]), " "), Valid: true},
					Country: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[5]), " "), Valid: true}}
				frsentries[frs.QRZ] = frs
			} else if re.MatchString(htmlquery.InnerText(td[0])) {
				frs := FrsEntry{
					QRZ:     strings.TrimLeft(htmlquery.InnerText(td[0]), " "),
					Name:    sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[1]), " "), Valid: true},
					City:    sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[2]), " "), Valid: true},
					Dept:    sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[3]), " "), Valid: true},
					Country: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[4]), " "), Valid: true}}
				frsentries[frs.QRZ] = frs
			}
		}
	}

	return
}

// getCurrentEntries fetch existing entries from database
func getCurrentEntries(config config.Config) (existingQRZ []FrsEntry, err error) {
	err = config.Db.Select(&existingQRZ, fmt.Sprintf("SELECT * FROM %s;", config.DbTable))

	return
}

// discardExistingEntries remove existing entries from original map[string]FrsEntry
func discardExistingEntries(config config.Config, frsPeople *map[string]FrsEntry, existingQRZ []FrsEntry) (err error) {
	for _, entry := range existingQRZ {
		delete(*frsPeople, entry.QRZ)
	}

	return
}

// insertFrsEntryToDB inserts frs entries to database
func insertFrsEntryToDB(config config.Config, frsPeople map[string]FrsEntry) (err error) {
	tx := config.Db.MustBegin()
	var qrzNum int

	fmt.Println(fmt.Sprintf("Starting inserts of %d entries", len(frsPeople)))

	for _, frs := range frsPeople {
		tx.MustExec(config.DbStatements.Insert, frs.QRZ, frs.DMRID, frs.Name, frs.City, frs.Dept, frs.Country)
		qrzNum++
	}

	err = tx.Commit()
	if err != nil {
		return err
	}
	fmt.Println(fmt.Sprintf("Committed %d qrz entries", qrzNum))

	return nil
}

// FrsEntry describe FRS people
type FrsEntry struct {
	ID      int            `db:"id"`
	QRZ     string         `db:"qrz"`
	DMRID   sql.NullString `db:"dmrid"`
	Name    sql.NullString `db:"name"`
	Address sql.NullString `db:"address"`
	City    sql.NullString `db:"city"`
	ZipCode sql.NullString `db:"zipcode"`
	Dept    sql.NullString `db:"dept"`
	Country sql.NullString `db:"country"`
	Fts     sql.NullString `db:"fts"`
}

// Qrz is the structure of qrz entries
type Qrz struct {
	gorm.Model
	Name         string
	Age          sql.NullInt64
	Birthday     *time.Time
	Email        string  `gorm:"type:varchar(100);unique_index"`
	Role         string  `gorm:"size:255"`        // set field size to 255
	MemberNumber *string `gorm:"unique;not null"` // set member number to unique and not null
	Num          int     `gorm:"AUTO_INCREMENT"`  // set num to auto incrementable
	Address      string  `gorm:"index:addr"`      // create index with name `addr` for address
	IgnoreMe     int     `gorm:"-"`               // ignore this field
}
